snow-flow 2.0.2 → 2.0.3
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/{.claude/mcp-config.json → .mcp.json} +13 -12
- package/.releaserc.json +47 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/.claude/claude-settings.json +0 -15
- package/.claude/commands/analysis/README.md +0 -9
- package/.claude/commands/automation/README.md +0 -9
- package/.claude/commands/github/README.md +0 -11
- package/.claude/commands/helpers/standard-checkpoint-hooks.sh +0 -179
- package/.claude/commands/hooks/README.md +0 -11
- package/.claude/commands/monitoring/README.md +0 -9
- package/.claude/commands/optimization/README.md +0 -9
- package/.claude/commands/training/README.md +0 -9
- package/.claude/commands/workflows/README.md +0 -9
- package/.claude/config.json +0 -36
- package/.claude/helpers/github-setup.sh +0 -28
- package/.claude/helpers/quick-start.sh +0 -19
- package/.claude/helpers/setup-mcp.sh +0 -18
- package/.claude/settings.json +0 -114
- package/.claude/settings.local.json +0 -33
- package/.claude-flow/metrics/agent-metrics.json +0 -1
- package/.claude-flow/metrics/performance.json +0 -9
- package/.claude-flow/metrics/system-metrics.json +0 -15986
- package/.claude-flow/metrics/task-metrics.json +0 -10
- package/.roo/README.md +0 -402
- package/.roo/mcp-list.txt +0 -257
- package/.roo/mcp.json +0 -32
- package/.roo/workflows/basic-tdd.json +0 -32
- package/.roomodes +0 -201
- package/CLAUDE.md +0 -393
- package/bin/snow-flow.js +0 -3
- package/dist/cli.js.bak +0 -2052
- package/dist/config/snow-flow-config.d.ts +0 -1492
- package/dist/config/snow-flow-config.js +0 -938
- package/scripts/create-env.js +0 -91
- package/scripts/postinstall.js +0 -30
- package/scripts/register-mcp-servers.js +0 -210
- package/scripts/reset-mcp-servers.js +0 -246
- package/scripts/setup-mcp.js +0 -106
- package/scripts/update-version.js +0 -31
- package/src/agents/README.md +0 -192
- package/src/agents/base-agent.ts +0 -251
- package/src/agents/coordinator.ts +0 -971
- package/src/agents/index.ts +0 -18
- package/src/agents/queen-403-handler.ts +0 -230
- package/src/agents/queen-agent.ts +0 -1399
- package/src/agents/script-writer-agent.ts +0 -769
- package/src/agents/security-agent.ts +0 -726
- package/src/agents/widget-creator-agent.ts +0 -568
- package/src/api/error-handling.ts +0 -1758
- package/src/api/natural-language-mapper.ts +0 -630
- package/src/api/performance-optimizer.ts +0 -891
- package/src/api/transaction-manager.ts +0 -754
- package/src/cli/deploy-artifact.ts +0 -304
- package/src/cli/snow-flow-cli-integration.ts +0 -434
- package/src/cli.ts +0 -4455
- package/src/cli.ts.bak +0 -5616
- package/src/compliance/advanced-compliance-system.ts +0 -1268
- package/src/compliance/index.ts +0 -19
- package/src/config/snow-flow-config.ts +0 -1019
- package/src/documentation/index.ts +0 -16
- package/src/documentation/self-documenting-system.ts +0 -1421
- package/src/dynamic-version.ts +0 -44
- package/src/healing/index.ts +0 -20
- package/src/healing/self-healing-system.ts +0 -1435
- package/src/health/README.md +0 -161
- package/src/health/system-health.ts +0 -995
- package/src/index.ts +0 -60
- package/src/intelligence/acl-analyzer.ts +0 -377
- package/src/intelligence/auto-resolution-engine.ts +0 -643
- package/src/intelligence/gap-analysis-engine.ts +0 -488
- package/src/intelligence/manual-instructions-generator.ts +0 -744
- package/src/intelligence/mcp-coverage-analyzer.ts +0 -708
- package/src/intelligence/multi-pass-requirements-analyzer.ts +0 -603
- package/src/intelligence/performance-recommendations-engine.ts +0 -1223
- package/src/intelligence/requirements-analyzer.ts +0 -1148
- package/src/intelligence/task-analyzer.ts +0 -628
- package/src/managers/scope-manager.ts +0 -555
- package/src/mcp/advanced/servicenow-advanced-features-mcp.ts +0 -16110
- package/src/mcp/base-mcp-server.ts +0 -660
- package/src/mcp/http-transport-wrapper.ts +0 -426
- package/src/mcp/service-discovery-client.ts +0 -312
- package/src/mcp/servicenow-automation-mcp-refactored.ts +0 -678
- package/src/mcp/servicenow-automation-mcp.ts +0 -838
- package/src/mcp/servicenow-deployment-mcp-refactored.ts +0 -1292
- package/src/mcp/servicenow-deployment-mcp.ts +0 -7244
- package/src/mcp/servicenow-graph-memory-mcp-refactored.ts +0 -721
- package/src/mcp/servicenow-graph-memory-mcp.ts +0 -832
- package/src/mcp/servicenow-integration-mcp-refactored.ts +0 -655
- package/src/mcp/servicenow-integration-mcp.ts +0 -819
- package/src/mcp/servicenow-intelligent-mcp-refactored.ts +0 -977
- package/src/mcp/servicenow-intelligent-mcp.ts +0 -4638
- package/src/mcp/servicenow-mcp-server.ts +0 -728
- package/src/mcp/servicenow-memory-mcp.ts +0 -474
- package/src/mcp/servicenow-operations-mcp-refactored.ts +0 -1995
- package/src/mcp/servicenow-operations-mcp.ts +0 -3384
- package/src/mcp/servicenow-platform-development-mcp-refactored.ts +0 -611
- package/src/mcp/servicenow-platform-development-mcp.ts +0 -970
- package/src/mcp/servicenow-progressive-indexer.ts +0 -370
- package/src/mcp/servicenow-reporting-analytics-mcp-refactored.ts +0 -695
- package/src/mcp/servicenow-reporting-analytics-mcp.ts +0 -1123
- package/src/mcp/servicenow-security-compliance-mcp-refactored.ts +0 -642
- package/src/mcp/servicenow-security-compliance-mcp.ts +0 -1157
- package/src/mcp/servicenow-update-set-mcp-refactored.ts +0 -751
- package/src/mcp/servicenow-update-set-mcp.ts +0 -811
- package/src/mcp/shared/agent-context-provider.ts +0 -365
- package/src/mcp/shared/base-mcp-server.ts +0 -2463
- package/src/mcp/shared/mcp-memory-manager.ts +0 -540
- package/src/mcp/shared/mcp-resource-manager.ts +0 -367
- package/src/mcp/snow-flow-mcp.ts +0 -911
- package/src/mcp/start-all-mcp-servers.ts +0 -102
- package/src/mcp/start-servicenow-mcp.ts +0 -67
- package/src/monitoring/enhanced-monitoring-system.ts +0 -1345
- package/src/monitoring/performance-tracker.ts +0 -777
- package/src/optimization/cost-optimization-engine.ts +0 -1125
- package/src/optimization/index.ts +0 -10
- package/src/queen/README.md +0 -403
- package/src/queen/agent-factory.ts +0 -574
- package/src/queen/index.ts +0 -94
- package/src/queen/mcp-execution-bridge.ts +0 -681
- package/src/queen/neural-learning.ts +0 -319
- package/src/queen/parallel-agent-engine.ts +0 -848
- package/src/queen/queen-memory-system.ts +0 -490
- package/src/queen/queen-memory.ts +0 -456
- package/src/queen/servicenow-queen.ts +0 -1029
- package/src/queen/types.ts +0 -87
- package/src/rollback/smart-rollback-system.ts +0 -866
- package/src/schemas/deployment.schema.json +0 -58
- package/src/schemas/flow.schema.json +0 -79
- package/src/schemas/widget.schema.json +0 -72
- package/src/snow-flow-system.ts +0 -541
- package/src/sparc/sparc-help.ts +0 -49
- package/src/sparc/team-sparc.ts +0 -88
- package/src/strategies/global-scope-strategy.ts +0 -736
- package/src/templates/base/application.template.json +0 -45
- package/src/templates/base/business_rule.template.json +0 -33
- package/src/templates/base/script_include.template.json +0 -18
- package/src/templates/base/table.template.json +0 -64
- package/src/templates/base/widget.template.json +0 -25
- package/src/templates/patterns/composite.incident-management.template.json +0 -140
- package/src/templates/patterns/widget.dashboard.template.json +0 -238
- package/src/templates/patterns/widget.datatable.template.json +0 -292
- package/src/testing/integration-test-suite.ts +0 -1276
- package/src/types/index.ts +0 -183
- package/src/types/neo4j-driver.d.ts +0 -23
- package/src/types/servicenow.types.ts +0 -307
- package/src/types/snow-flow.types.ts +0 -186
- package/src/types/todo.types.ts +0 -70
- package/src/utils/action-type-cache.ts +0 -286
- package/src/utils/agent-detector.ts +0 -573
- package/src/utils/artifact-tracker.ts +0 -379
- package/src/utils/dependency-detector.ts +0 -233
- package/src/utils/deployment-metadata-handler.ts +0 -518
- package/src/utils/error-recovery.ts +0 -680
- package/src/utils/logger.ts +0 -60
- package/src/utils/mcp-auth-middleware.ts +0 -262
- package/src/utils/mcp-config-manager.ts +0 -270
- package/src/utils/mcp-server-manager.ts +0 -446
- package/src/utils/mcp-tool-registry.ts +0 -298
- package/src/utils/migrate-snow-flow.ts +0 -98
- package/src/utils/oauth-html-templates.ts +0 -188
- package/src/utils/scope-utils.ts +0 -443
- package/src/utils/servicenow-client.ts +0 -3321
- package/src/utils/servicenow-id-generator.ts +0 -181
- package/src/utils/snow-memory-manager.ts +0 -605
- package/src/utils/snow-oauth.ts +0 -797
- package/src/utils/template-engine.ts +0 -463
- package/src/utils/theme-manager.ts +0 -295
- package/src/utils/unified-auth-store.ts +0 -222
- package/src/utils/update-set-importer.ts +0 -422
- package/src/utils/update-set-xml-packager.ts +0 -371
- package/src/utils/widget-template-generator.ts +0 -1744
- package/src/version.ts +0 -1950
|
@@ -1,3384 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* ServiceNow Operations MCP Server
|
|
5
|
-
*
|
|
6
|
-
* Handles operational data queries, incident management, and intelligent analysis
|
|
7
|
-
* Features:
|
|
8
|
-
* - Incident, Request, Problem, Change management
|
|
9
|
-
* - CMDB and User management
|
|
10
|
-
* - Intelligent incident _analysis and auto-resolution
|
|
11
|
-
* - Pattern recognition and root cause analysis
|
|
12
|
-
* - Knowledge base integration
|
|
13
|
-
* - Predictive analytics
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
18
|
-
import {
|
|
19
|
-
CallToolRequestSchema,
|
|
20
|
-
ErrorCode,
|
|
21
|
-
ListToolsRequestSchema,
|
|
22
|
-
McpError,
|
|
23
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
24
|
-
import { ServiceNowClient } from '../utils/servicenow-client.js';
|
|
25
|
-
import { logger } from '../utils/logger.js';
|
|
26
|
-
|
|
27
|
-
// Operational table mappings for ServiceNow
|
|
28
|
-
const operationalTableMapping = {
|
|
29
|
-
// ITIL Core Processes
|
|
30
|
-
incident: 'incident',
|
|
31
|
-
request: 'sc_request',
|
|
32
|
-
request_item: 'sc_req_item',
|
|
33
|
-
problem: 'problem',
|
|
34
|
-
change_request: 'change_request',
|
|
35
|
-
|
|
36
|
-
// Service Catalog
|
|
37
|
-
catalog: 'sc_catalog',
|
|
38
|
-
catalog_item: 'sc_cat_item',
|
|
39
|
-
catalog_category: 'sc_category',
|
|
40
|
-
catalog_variable: 'item_option_new',
|
|
41
|
-
catalog_variable_set: 'io_set_item',
|
|
42
|
-
catalog_ui_policy: 'catalog_ui_policy',
|
|
43
|
-
catalog_client_script: 'catalog_script_client',
|
|
44
|
-
catalog_template: 'sc_template',
|
|
45
|
-
|
|
46
|
-
// Task Management
|
|
47
|
-
task: 'task',
|
|
48
|
-
incident_task: 'incident_task',
|
|
49
|
-
problem_task: 'problem_task',
|
|
50
|
-
change_task: 'change_task',
|
|
51
|
-
approval: 'sysapproval_approver',
|
|
52
|
-
|
|
53
|
-
// User & Group Management
|
|
54
|
-
user: 'sys_user',
|
|
55
|
-
user_group: 'sys_user_group',
|
|
56
|
-
user_role: 'sys_user_has_role',
|
|
57
|
-
|
|
58
|
-
// CMDB (Configuration Management Database)
|
|
59
|
-
configuration_item: 'cmdb_ci',
|
|
60
|
-
server: 'cmdb_ci_server',
|
|
61
|
-
application: 'cmdb_ci_application',
|
|
62
|
-
database: 'cmdb_ci_database',
|
|
63
|
-
network_device: 'cmdb_ci_network_device',
|
|
64
|
-
service: 'cmdb_ci_service',
|
|
65
|
-
|
|
66
|
-
// Logging & Audit
|
|
67
|
-
system_log: 'syslog',
|
|
68
|
-
audit_log: 'sys_audit',
|
|
69
|
-
email: 'sys_email',
|
|
70
|
-
attachment: 'sys_attachment',
|
|
71
|
-
|
|
72
|
-
// Knowledge Management
|
|
73
|
-
knowledge_base: 'kb_knowledge_base',
|
|
74
|
-
knowledge: 'kb_knowledge',
|
|
75
|
-
|
|
76
|
-
// Metrics & Analytics
|
|
77
|
-
metric: 'sys_metric',
|
|
78
|
-
metric_instance: 'sys_metric_instance',
|
|
79
|
-
|
|
80
|
-
// Notifications
|
|
81
|
-
notification: 'sysevent_email_action',
|
|
82
|
-
email_log: 'sys_email'
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// Common operational queries for intelligent analysis
|
|
86
|
-
const commonPatterns = {
|
|
87
|
-
// Network connectivity issues
|
|
88
|
-
network_issues: {
|
|
89
|
-
keywords: ['network', 'connectivity', 'ping', 'dns', 'timeout', 'unreachable'],
|
|
90
|
-
common_solutions: [
|
|
91
|
-
'Check network connectivity',
|
|
92
|
-
'Verify DNS resolution',
|
|
93
|
-
'Test ping to server',
|
|
94
|
-
'Review firewall rules'
|
|
95
|
-
]
|
|
96
|
-
},
|
|
97
|
-
|
|
98
|
-
// Database connectivity
|
|
99
|
-
database_issues: {
|
|
100
|
-
keywords: ['database', 'connection', 'sql', 'timeout', 'deadlock', 'performance'],
|
|
101
|
-
common_solutions: [
|
|
102
|
-
'Check database connection',
|
|
103
|
-
'Review connection pool settings',
|
|
104
|
-
'Analyze query performance',
|
|
105
|
-
'Check for blocking processes'
|
|
106
|
-
]
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
// Application errors
|
|
110
|
-
application_errors: {
|
|
111
|
-
keywords: ['application', 'error', 'exception', 'crash', 'memory', 'cpu'],
|
|
112
|
-
common_solutions: [
|
|
113
|
-
'Check application logs',
|
|
114
|
-
'Review memory usage',
|
|
115
|
-
'Analyze CPU utilization',
|
|
116
|
-
'Restart application service'
|
|
117
|
-
]
|
|
118
|
-
},
|
|
119
|
-
|
|
120
|
-
// Authentication issues
|
|
121
|
-
auth_issues: {
|
|
122
|
-
keywords: ['authentication', 'login', 'password', 'ldap', 'sso', 'unauthorized'],
|
|
123
|
-
common_solutions: [
|
|
124
|
-
'Reset user password',
|
|
125
|
-
'Check LDAP connectivity',
|
|
126
|
-
'Verify SSO configuration',
|
|
127
|
-
'Review user permissions'
|
|
128
|
-
]
|
|
129
|
-
}
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
interface IncidentAnalysis {
|
|
133
|
-
incident_id: string;
|
|
134
|
-
patterns_found: string[];
|
|
135
|
-
root_cause__analysis: string;
|
|
136
|
-
suggested_resolution: string[];
|
|
137
|
-
confidence_score: number;
|
|
138
|
-
similar_incidents: any[];
|
|
139
|
-
knowledge_articles: any[];
|
|
140
|
-
automated_actions: string[];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface OperationalMetrics {
|
|
144
|
-
total_incidents: number;
|
|
145
|
-
open_incidents: number;
|
|
146
|
-
high_priority_incidents: number;
|
|
147
|
-
avg_resolution_time: number;
|
|
148
|
-
common_categories: string[];
|
|
149
|
-
trending_issues: string[];
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
class ServiceNowOperationsMCP {
|
|
153
|
-
private server: Server;
|
|
154
|
-
private client: ServiceNowClient;
|
|
155
|
-
|
|
156
|
-
constructor() {
|
|
157
|
-
this.server = new Server(
|
|
158
|
-
{
|
|
159
|
-
name: 'servicenow-operations-mcp',
|
|
160
|
-
version: '1.0.0',
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
capabilities: {
|
|
164
|
-
tools: {},
|
|
165
|
-
},
|
|
166
|
-
}
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
this.client = new ServiceNowClient();
|
|
170
|
-
this.setupToolHandlers();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
private setupToolHandlers() {
|
|
174
|
-
// List available tools
|
|
175
|
-
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
176
|
-
return {
|
|
177
|
-
tools: [
|
|
178
|
-
// Core Operational Queries
|
|
179
|
-
{
|
|
180
|
-
name: 'snow_query_incidents',
|
|
181
|
-
description: 'Advanced incident querying with filters and _analysis',
|
|
182
|
-
inputSchema: {
|
|
183
|
-
type: 'object',
|
|
184
|
-
properties: {
|
|
185
|
-
query: {
|
|
186
|
-
type: 'string',
|
|
187
|
-
description: 'ServiceNow encoded query or natural language description'
|
|
188
|
-
},
|
|
189
|
-
limit: {
|
|
190
|
-
type: 'number',
|
|
191
|
-
description: 'Maximum number of results (default: 10)',
|
|
192
|
-
default: 10
|
|
193
|
-
},
|
|
194
|
-
include__analysis: {
|
|
195
|
-
type: 'boolean',
|
|
196
|
-
description: 'Include intelligent _analysis of incidents',
|
|
197
|
-
default: false
|
|
198
|
-
},
|
|
199
|
-
fields: {
|
|
200
|
-
type: 'array',
|
|
201
|
-
items: { type: 'string' },
|
|
202
|
-
description: 'Specific fields to return'
|
|
203
|
-
}
|
|
204
|
-
},
|
|
205
|
-
required: ['query']
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
name: 'snow_analyze_incident',
|
|
211
|
-
description: 'Intelligent _analysis of a specific incident with auto-resolution suggestions',
|
|
212
|
-
inputSchema: {
|
|
213
|
-
type: 'object',
|
|
214
|
-
properties: {
|
|
215
|
-
incident_id: {
|
|
216
|
-
type: 'string',
|
|
217
|
-
description: 'Incident number or sys_id'
|
|
218
|
-
},
|
|
219
|
-
include_similar: {
|
|
220
|
-
type: 'boolean',
|
|
221
|
-
description: 'Include similar incidents in _analysis',
|
|
222
|
-
default: true
|
|
223
|
-
},
|
|
224
|
-
suggest_resolution: {
|
|
225
|
-
type: 'boolean',
|
|
226
|
-
description: 'Generate automated resolution suggestions',
|
|
227
|
-
default: true
|
|
228
|
-
}
|
|
229
|
-
},
|
|
230
|
-
required: ['incident_id']
|
|
231
|
-
}
|
|
232
|
-
},
|
|
233
|
-
|
|
234
|
-
{
|
|
235
|
-
name: 'snow_auto_resolve_incident',
|
|
236
|
-
description: 'Attempt automated resolution of technical incidents',
|
|
237
|
-
inputSchema: {
|
|
238
|
-
type: 'object',
|
|
239
|
-
properties: {
|
|
240
|
-
incident_id: {
|
|
241
|
-
type: 'string',
|
|
242
|
-
description: 'Incident number or sys_id'
|
|
243
|
-
},
|
|
244
|
-
dry_run: {
|
|
245
|
-
type: 'boolean',
|
|
246
|
-
description: 'Preview actions without executing',
|
|
247
|
-
default: true
|
|
248
|
-
}
|
|
249
|
-
},
|
|
250
|
-
required: ['incident_id']
|
|
251
|
-
}
|
|
252
|
-
},
|
|
253
|
-
|
|
254
|
-
{
|
|
255
|
-
name: 'snow_query_requests',
|
|
256
|
-
description: 'Query and analyze service requests',
|
|
257
|
-
inputSchema: {
|
|
258
|
-
type: 'object',
|
|
259
|
-
properties: {
|
|
260
|
-
query: {
|
|
261
|
-
type: 'string',
|
|
262
|
-
description: 'ServiceNow encoded query or natural language'
|
|
263
|
-
},
|
|
264
|
-
limit: {
|
|
265
|
-
type: 'number',
|
|
266
|
-
description: 'Maximum number of results',
|
|
267
|
-
default: 10
|
|
268
|
-
},
|
|
269
|
-
include_items: {
|
|
270
|
-
type: 'boolean',
|
|
271
|
-
description: 'Include request items',
|
|
272
|
-
default: false
|
|
273
|
-
}
|
|
274
|
-
},
|
|
275
|
-
required: ['query']
|
|
276
|
-
}
|
|
277
|
-
},
|
|
278
|
-
|
|
279
|
-
{
|
|
280
|
-
name: 'snow_query_problems',
|
|
281
|
-
description: 'Query and analyze problems with root cause _analysis',
|
|
282
|
-
inputSchema: {
|
|
283
|
-
type: 'object',
|
|
284
|
-
properties: {
|
|
285
|
-
query: {
|
|
286
|
-
type: 'string',
|
|
287
|
-
description: 'ServiceNow encoded query or natural language'
|
|
288
|
-
},
|
|
289
|
-
limit: {
|
|
290
|
-
type: 'number',
|
|
291
|
-
description: 'Maximum number of results',
|
|
292
|
-
default: 10
|
|
293
|
-
},
|
|
294
|
-
include_incidents: {
|
|
295
|
-
type: 'boolean',
|
|
296
|
-
description: 'Include related incidents',
|
|
297
|
-
default: false
|
|
298
|
-
}
|
|
299
|
-
},
|
|
300
|
-
required: ['query']
|
|
301
|
-
}
|
|
302
|
-
},
|
|
303
|
-
|
|
304
|
-
{
|
|
305
|
-
name: 'snow_cmdb_search',
|
|
306
|
-
description: 'Search and analyze Configuration Items (CMDB)',
|
|
307
|
-
inputSchema: {
|
|
308
|
-
type: 'object',
|
|
309
|
-
properties: {
|
|
310
|
-
query: {
|
|
311
|
-
type: 'string',
|
|
312
|
-
description: 'Search query for configuration items'
|
|
313
|
-
},
|
|
314
|
-
ci_type: {
|
|
315
|
-
type: 'string',
|
|
316
|
-
description: 'Type of CI (server, application, service, etc.)',
|
|
317
|
-
enum: ['server', 'application', 'database', 'network_device', 'service', 'any']
|
|
318
|
-
},
|
|
319
|
-
limit: {
|
|
320
|
-
type: 'number',
|
|
321
|
-
description: 'Maximum number of results',
|
|
322
|
-
default: 10
|
|
323
|
-
},
|
|
324
|
-
include_relationships: {
|
|
325
|
-
type: 'boolean',
|
|
326
|
-
description: 'Include CI relationships',
|
|
327
|
-
default: false
|
|
328
|
-
}
|
|
329
|
-
},
|
|
330
|
-
required: ['query']
|
|
331
|
-
}
|
|
332
|
-
},
|
|
333
|
-
|
|
334
|
-
{
|
|
335
|
-
name: 'snow_user_lookup',
|
|
336
|
-
description: 'Lookup and analyze user information',
|
|
337
|
-
inputSchema: {
|
|
338
|
-
type: 'object',
|
|
339
|
-
properties: {
|
|
340
|
-
identifier: {
|
|
341
|
-
type: 'string',
|
|
342
|
-
description: 'User ID, email, or name'
|
|
343
|
-
},
|
|
344
|
-
include_roles: {
|
|
345
|
-
type: 'boolean',
|
|
346
|
-
description: 'Include user roles',
|
|
347
|
-
default: true
|
|
348
|
-
},
|
|
349
|
-
include_groups: {
|
|
350
|
-
type: 'boolean',
|
|
351
|
-
description: 'Include user groups',
|
|
352
|
-
default: true
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
required: ['identifier']
|
|
356
|
-
}
|
|
357
|
-
},
|
|
358
|
-
|
|
359
|
-
{
|
|
360
|
-
name: 'snow_operational_metrics',
|
|
361
|
-
description: 'Get operational metrics and analytics',
|
|
362
|
-
inputSchema: {
|
|
363
|
-
type: 'object',
|
|
364
|
-
properties: {
|
|
365
|
-
timeframe: {
|
|
366
|
-
type: 'string',
|
|
367
|
-
description: 'Time period for metrics',
|
|
368
|
-
enum: ['today', 'week', 'month', 'quarter'],
|
|
369
|
-
default: 'week'
|
|
370
|
-
},
|
|
371
|
-
metric_types: {
|
|
372
|
-
type: 'array',
|
|
373
|
-
items: { type: 'string' },
|
|
374
|
-
description: 'Types of metrics to include'
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
},
|
|
379
|
-
|
|
380
|
-
{
|
|
381
|
-
name: 'snow_pattern__analysis',
|
|
382
|
-
description: 'Analyze patterns in incidents, requests, and problems',
|
|
383
|
-
inputSchema: {
|
|
384
|
-
type: 'object',
|
|
385
|
-
properties: {
|
|
386
|
-
analysis_type: {
|
|
387
|
-
type: 'string',
|
|
388
|
-
description: 'Type of pattern _analysis',
|
|
389
|
-
enum: ['incident_patterns', 'request_trends', 'problem_root_causes', 'user_behavior']
|
|
390
|
-
},
|
|
391
|
-
timeframe: {
|
|
392
|
-
type: 'string',
|
|
393
|
-
description: 'Time period for _analysis',
|
|
394
|
-
enum: ['day', 'week', 'month', 'quarter'],
|
|
395
|
-
default: 'week'
|
|
396
|
-
}
|
|
397
|
-
},
|
|
398
|
-
required: ['analysis_type']
|
|
399
|
-
}
|
|
400
|
-
},
|
|
401
|
-
|
|
402
|
-
{
|
|
403
|
-
name: 'snow_knowledge_search',
|
|
404
|
-
description: 'Search knowledge base articles with intelligent matching',
|
|
405
|
-
inputSchema: {
|
|
406
|
-
type: 'object',
|
|
407
|
-
properties: {
|
|
408
|
-
query: {
|
|
409
|
-
type: 'string',
|
|
410
|
-
description: 'Search query or incident description'
|
|
411
|
-
},
|
|
412
|
-
match_incident: {
|
|
413
|
-
type: 'string',
|
|
414
|
-
description: 'Match knowledge to specific incident'
|
|
415
|
-
},
|
|
416
|
-
limit: {
|
|
417
|
-
type: 'number',
|
|
418
|
-
description: 'Maximum number of results',
|
|
419
|
-
default: 5
|
|
420
|
-
}
|
|
421
|
-
},
|
|
422
|
-
required: ['query']
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
|
|
426
|
-
{
|
|
427
|
-
name: 'snow_predictive__analysis',
|
|
428
|
-
description: 'Predictive _analysis for potential issues and trends',
|
|
429
|
-
inputSchema: {
|
|
430
|
-
type: 'object',
|
|
431
|
-
properties: {
|
|
432
|
-
prediction_type: {
|
|
433
|
-
type: 'string',
|
|
434
|
-
description: 'Type of prediction',
|
|
435
|
-
enum: ['incident_volume', 'system_failure', 'resource_exhaustion', 'user_impact']
|
|
436
|
-
},
|
|
437
|
-
timeframe: {
|
|
438
|
-
type: 'string',
|
|
439
|
-
description: 'Prediction timeframe',
|
|
440
|
-
enum: ['day', 'week', 'month'],
|
|
441
|
-
default: 'week'
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
|
-
required: ['prediction_type']
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
|
|
448
|
-
// Service Catalog Management
|
|
449
|
-
{
|
|
450
|
-
name: 'snow_catalog_item_manager',
|
|
451
|
-
description: 'Manage service catalog items - create, update, configure variables and workflows',
|
|
452
|
-
inputSchema: {
|
|
453
|
-
type: 'object',
|
|
454
|
-
properties: {
|
|
455
|
-
action: {
|
|
456
|
-
type: 'string',
|
|
457
|
-
description: 'Action to perform',
|
|
458
|
-
enum: ['create', 'update', 'list', 'get', 'add_variable', 'set_workflow', 'publish', 'retire']
|
|
459
|
-
},
|
|
460
|
-
catalog_id: {
|
|
461
|
-
type: 'string',
|
|
462
|
-
description: 'Catalog sys_id (for create action)'
|
|
463
|
-
},
|
|
464
|
-
category_id: {
|
|
465
|
-
type: 'string',
|
|
466
|
-
description: 'Category sys_id'
|
|
467
|
-
},
|
|
468
|
-
item_id: {
|
|
469
|
-
type: 'string',
|
|
470
|
-
description: 'Catalog item sys_id (for update/get actions)'
|
|
471
|
-
},
|
|
472
|
-
name: {
|
|
473
|
-
type: 'string',
|
|
474
|
-
description: 'Catalog item name'
|
|
475
|
-
},
|
|
476
|
-
short_description: {
|
|
477
|
-
type: 'string',
|
|
478
|
-
description: 'Short description'
|
|
479
|
-
},
|
|
480
|
-
description: {
|
|
481
|
-
type: 'string',
|
|
482
|
-
description: 'Full description'
|
|
483
|
-
},
|
|
484
|
-
price: {
|
|
485
|
-
type: 'string',
|
|
486
|
-
description: 'Item price (e.g., "100.00")'
|
|
487
|
-
},
|
|
488
|
-
recurring_price: {
|
|
489
|
-
type: 'string',
|
|
490
|
-
description: 'Recurring price if applicable'
|
|
491
|
-
},
|
|
492
|
-
picture: {
|
|
493
|
-
type: 'string',
|
|
494
|
-
description: 'Picture attachment sys_id'
|
|
495
|
-
},
|
|
496
|
-
workflow: {
|
|
497
|
-
type: 'string',
|
|
498
|
-
description: 'Workflow to attach'
|
|
499
|
-
},
|
|
500
|
-
variable_set: {
|
|
501
|
-
type: 'string',
|
|
502
|
-
description: 'Variable set sys_id to attach'
|
|
503
|
-
},
|
|
504
|
-
variables: {
|
|
505
|
-
type: 'array',
|
|
506
|
-
description: 'Variables to add',
|
|
507
|
-
items: {
|
|
508
|
-
type: 'object',
|
|
509
|
-
properties: {
|
|
510
|
-
name: { type: 'string' },
|
|
511
|
-
label: { type: 'string' },
|
|
512
|
-
type: {
|
|
513
|
-
type: 'string',
|
|
514
|
-
enum: ['single_line_text', 'multi_line_text', 'select_box', 'checkbox', 'reference', 'date', 'datetime']
|
|
515
|
-
},
|
|
516
|
-
mandatory: { type: 'boolean' },
|
|
517
|
-
default_value: { type: 'string' },
|
|
518
|
-
choices: { type: 'array', items: { type: 'string' } }
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
},
|
|
522
|
-
active: {
|
|
523
|
-
type: 'boolean',
|
|
524
|
-
description: 'Whether the item is active',
|
|
525
|
-
default: true
|
|
526
|
-
},
|
|
527
|
-
billable: {
|
|
528
|
-
type: 'boolean',
|
|
529
|
-
description: 'Whether the item is billable',
|
|
530
|
-
default: false
|
|
531
|
-
},
|
|
532
|
-
sc_catalogs: {
|
|
533
|
-
type: 'string',
|
|
534
|
-
description: 'Comma-separated catalog sys_ids'
|
|
535
|
-
},
|
|
536
|
-
sc_categories: {
|
|
537
|
-
type: 'string',
|
|
538
|
-
description: 'Comma-separated category sys_ids'
|
|
539
|
-
}
|
|
540
|
-
},
|
|
541
|
-
required: ['action']
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
|
|
545
|
-
{
|
|
546
|
-
name: 'snow_catalog_item_search',
|
|
547
|
-
description: 'Search for catalog items with intelligent matching and discovery',
|
|
548
|
-
inputSchema: {
|
|
549
|
-
type: 'object',
|
|
550
|
-
properties: {
|
|
551
|
-
query: {
|
|
552
|
-
type: 'string',
|
|
553
|
-
description: 'Search query (e.g., "iPhone", "laptop", "mobile device")'
|
|
554
|
-
},
|
|
555
|
-
include_inactive: {
|
|
556
|
-
type: 'boolean',
|
|
557
|
-
description: 'Include inactive catalog items',
|
|
558
|
-
default: false
|
|
559
|
-
},
|
|
560
|
-
fuzzy_match: {
|
|
561
|
-
type: 'boolean',
|
|
562
|
-
description: 'Enable fuzzy matching for similar items',
|
|
563
|
-
default: true
|
|
564
|
-
},
|
|
565
|
-
category_filter: {
|
|
566
|
-
type: 'string',
|
|
567
|
-
description: 'Filter by category name or sys_id'
|
|
568
|
-
},
|
|
569
|
-
limit: {
|
|
570
|
-
type: 'number',
|
|
571
|
-
description: 'Maximum results to return',
|
|
572
|
-
default: 50
|
|
573
|
-
},
|
|
574
|
-
include_variables: {
|
|
575
|
-
type: 'boolean',
|
|
576
|
-
description: 'Include catalog item variables in results',
|
|
577
|
-
default: false
|
|
578
|
-
}
|
|
579
|
-
},
|
|
580
|
-
required: ['query']
|
|
581
|
-
}
|
|
582
|
-
},
|
|
583
|
-
|
|
584
|
-
{
|
|
585
|
-
name: 'snow_cleanup_test_artifacts',
|
|
586
|
-
description: 'Clean up test artifacts while preserving Update Set audit trail',
|
|
587
|
-
inputSchema: {
|
|
588
|
-
type: 'object',
|
|
589
|
-
properties: {
|
|
590
|
-
artifact_types: {
|
|
591
|
-
type: 'array',
|
|
592
|
-
description: 'Types of artifacts to clean up',
|
|
593
|
-
items: {
|
|
594
|
-
type: 'string',
|
|
595
|
-
enum: ['catalog_items', 'flows', 'users', 'requests', 'all']
|
|
596
|
-
},
|
|
597
|
-
default: ['catalog_items', 'flows', 'users']
|
|
598
|
-
},
|
|
599
|
-
test_patterns: {
|
|
600
|
-
type: 'array',
|
|
601
|
-
description: 'Name patterns that identify test artifacts',
|
|
602
|
-
items: { type: 'string' },
|
|
603
|
-
default: ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock']
|
|
604
|
-
},
|
|
605
|
-
max_age_hours: {
|
|
606
|
-
type: 'number',
|
|
607
|
-
description: 'Only clean artifacts older than this (hours)',
|
|
608
|
-
default: 1
|
|
609
|
-
},
|
|
610
|
-
dry_run: {
|
|
611
|
-
type: 'boolean',
|
|
612
|
-
description: 'Preview what would be deleted without actually deleting',
|
|
613
|
-
default: false
|
|
614
|
-
},
|
|
615
|
-
preserve_update_set_entries: {
|
|
616
|
-
type: 'boolean',
|
|
617
|
-
description: 'Keep Update Set entries as audit trail',
|
|
618
|
-
default: true
|
|
619
|
-
},
|
|
620
|
-
update_set_filter: {
|
|
621
|
-
type: 'string',
|
|
622
|
-
description: 'Only clean from specific Update Set (optional)'
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
},
|
|
627
|
-
|
|
628
|
-
// User and Group Management Tools
|
|
629
|
-
{
|
|
630
|
-
name: 'snow_create_user_group',
|
|
631
|
-
description: 'Create a new user group in ServiceNow',
|
|
632
|
-
inputSchema: {
|
|
633
|
-
type: 'object',
|
|
634
|
-
properties: {
|
|
635
|
-
name: {
|
|
636
|
-
type: 'string',
|
|
637
|
-
description: 'Group name (e.g., "Need Approval")'
|
|
638
|
-
},
|
|
639
|
-
description: {
|
|
640
|
-
type: 'string',
|
|
641
|
-
description: 'Group description'
|
|
642
|
-
},
|
|
643
|
-
email: {
|
|
644
|
-
type: 'string',
|
|
645
|
-
description: 'Group email address (optional)'
|
|
646
|
-
},
|
|
647
|
-
manager: {
|
|
648
|
-
type: 'string',
|
|
649
|
-
description: 'Group manager sys_id or user_name (optional)'
|
|
650
|
-
},
|
|
651
|
-
parent: {
|
|
652
|
-
type: 'string',
|
|
653
|
-
description: 'Parent group sys_id (optional)'
|
|
654
|
-
},
|
|
655
|
-
type: {
|
|
656
|
-
type: 'string',
|
|
657
|
-
description: 'Group type (optional)',
|
|
658
|
-
enum: ['', 'catalog', 'change', 'incident', 'problem', 'request']
|
|
659
|
-
},
|
|
660
|
-
active: {
|
|
661
|
-
type: 'boolean',
|
|
662
|
-
description: 'Is group active',
|
|
663
|
-
default: true
|
|
664
|
-
}
|
|
665
|
-
},
|
|
666
|
-
required: ['name']
|
|
667
|
-
}
|
|
668
|
-
},
|
|
669
|
-
|
|
670
|
-
{
|
|
671
|
-
name: 'snow_create_user',
|
|
672
|
-
description: 'Create a new user in ServiceNow',
|
|
673
|
-
inputSchema: {
|
|
674
|
-
type: 'object',
|
|
675
|
-
properties: {
|
|
676
|
-
user_name: {
|
|
677
|
-
type: 'string',
|
|
678
|
-
description: 'Unique username for login'
|
|
679
|
-
},
|
|
680
|
-
first_name: {
|
|
681
|
-
type: 'string',
|
|
682
|
-
description: 'User first name'
|
|
683
|
-
},
|
|
684
|
-
last_name: {
|
|
685
|
-
type: 'string',
|
|
686
|
-
description: 'User last name'
|
|
687
|
-
},
|
|
688
|
-
email: {
|
|
689
|
-
type: 'string',
|
|
690
|
-
description: 'User email address'
|
|
691
|
-
},
|
|
692
|
-
department: {
|
|
693
|
-
type: 'string',
|
|
694
|
-
description: 'Department name or sys_id (optional)'
|
|
695
|
-
},
|
|
696
|
-
manager: {
|
|
697
|
-
type: 'string',
|
|
698
|
-
description: 'Manager sys_id or user_name (optional)'
|
|
699
|
-
},
|
|
700
|
-
phone: {
|
|
701
|
-
type: 'string',
|
|
702
|
-
description: 'Phone number (optional)'
|
|
703
|
-
},
|
|
704
|
-
title: {
|
|
705
|
-
type: 'string',
|
|
706
|
-
description: 'Job title (optional)'
|
|
707
|
-
},
|
|
708
|
-
location: {
|
|
709
|
-
type: 'string',
|
|
710
|
-
description: 'Location name or sys_id (optional)'
|
|
711
|
-
},
|
|
712
|
-
active: {
|
|
713
|
-
type: 'boolean',
|
|
714
|
-
description: 'Is user active',
|
|
715
|
-
default: true
|
|
716
|
-
},
|
|
717
|
-
password: {
|
|
718
|
-
type: 'string',
|
|
719
|
-
description: 'Initial password (will require change on first login)'
|
|
720
|
-
}
|
|
721
|
-
},
|
|
722
|
-
required: ['user_name', 'first_name', 'last_name', 'email']
|
|
723
|
-
}
|
|
724
|
-
},
|
|
725
|
-
|
|
726
|
-
{
|
|
727
|
-
name: 'snow_assign_user_to_group',
|
|
728
|
-
description: 'Add a user to a group in ServiceNow',
|
|
729
|
-
inputSchema: {
|
|
730
|
-
type: 'object',
|
|
731
|
-
properties: {
|
|
732
|
-
user: {
|
|
733
|
-
type: 'string',
|
|
734
|
-
description: 'User sys_id or user_name'
|
|
735
|
-
},
|
|
736
|
-
group: {
|
|
737
|
-
type: 'string',
|
|
738
|
-
description: 'Group sys_id or name'
|
|
739
|
-
}
|
|
740
|
-
},
|
|
741
|
-
required: ['user', 'group']
|
|
742
|
-
}
|
|
743
|
-
},
|
|
744
|
-
|
|
745
|
-
{
|
|
746
|
-
name: 'snow_remove_user_from_group',
|
|
747
|
-
description: 'Remove a user from a group in ServiceNow',
|
|
748
|
-
inputSchema: {
|
|
749
|
-
type: 'object',
|
|
750
|
-
properties: {
|
|
751
|
-
user: {
|
|
752
|
-
type: 'string',
|
|
753
|
-
description: 'User sys_id or user_name'
|
|
754
|
-
},
|
|
755
|
-
group: {
|
|
756
|
-
type: 'string',
|
|
757
|
-
description: 'Group sys_id or name'
|
|
758
|
-
}
|
|
759
|
-
},
|
|
760
|
-
required: ['user', 'group']
|
|
761
|
-
}
|
|
762
|
-
},
|
|
763
|
-
|
|
764
|
-
{
|
|
765
|
-
name: 'snow_list_group_members',
|
|
766
|
-
description: 'List all members of a group',
|
|
767
|
-
inputSchema: {
|
|
768
|
-
type: 'object',
|
|
769
|
-
properties: {
|
|
770
|
-
group: {
|
|
771
|
-
type: 'string',
|
|
772
|
-
description: 'Group sys_id or name'
|
|
773
|
-
},
|
|
774
|
-
active_only: {
|
|
775
|
-
type: 'boolean',
|
|
776
|
-
description: 'Only show active users',
|
|
777
|
-
default: true
|
|
778
|
-
}
|
|
779
|
-
},
|
|
780
|
-
required: ['group']
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
],
|
|
784
|
-
};
|
|
785
|
-
});
|
|
786
|
-
|
|
787
|
-
// Handle tool calls
|
|
788
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
789
|
-
const { name, arguments: args } = request.params;
|
|
790
|
-
|
|
791
|
-
try {
|
|
792
|
-
switch (name) {
|
|
793
|
-
case 'snow_query_incidents':
|
|
794
|
-
return await this.handleQueryIncidents(args);
|
|
795
|
-
case 'snow_analyze_incident':
|
|
796
|
-
return await this.handleAnalyzeIncident(args);
|
|
797
|
-
case 'snow_auto_resolve_incident':
|
|
798
|
-
return await this.handleAutoResolveIncident(args);
|
|
799
|
-
case 'snow_query_requests':
|
|
800
|
-
return await this.handleQueryRequests(args);
|
|
801
|
-
case 'snow_query_problems':
|
|
802
|
-
return await this.handleQueryProblems(args);
|
|
803
|
-
case 'snow_cmdb_search':
|
|
804
|
-
return await this.handleCMDBSearch(args);
|
|
805
|
-
case 'snow_user_lookup':
|
|
806
|
-
return await this.handleUserLookup(args);
|
|
807
|
-
case 'snow_operational_metrics':
|
|
808
|
-
return await this.handleOperationalMetrics(args);
|
|
809
|
-
case 'snow_pattern__analysis':
|
|
810
|
-
return await this.handlePatternAnalysis(args);
|
|
811
|
-
case 'snow_knowledge_search':
|
|
812
|
-
return await this.handleKnowledgeSearch(args);
|
|
813
|
-
case 'snow_predictive__analysis':
|
|
814
|
-
return await this.handlePredictiveAnalysis(args);
|
|
815
|
-
case 'snow_catalog_item_manager':
|
|
816
|
-
return await this.handleCatalogItemManager(args);
|
|
817
|
-
case 'snow_catalog_item_search':
|
|
818
|
-
return await this.handleCatalogItemSearch(args);
|
|
819
|
-
case 'snow_cleanup_test_artifacts':
|
|
820
|
-
return await this.handleCleanupTestArtifacts(args);
|
|
821
|
-
case 'snow_create_user_group':
|
|
822
|
-
return await this.handleCreateUserGroup(args);
|
|
823
|
-
case 'snow_create_user':
|
|
824
|
-
return await this.handleCreateUser(args);
|
|
825
|
-
case 'snow_assign_user_to_group':
|
|
826
|
-
return await this.handleAssignUserToGroup(args);
|
|
827
|
-
case 'snow_remove_user_from_group':
|
|
828
|
-
return await this.handleRemoveUserFromGroup(args);
|
|
829
|
-
case 'snow_list_group_members':
|
|
830
|
-
return await this.handleListGroupMembers(args);
|
|
831
|
-
default:
|
|
832
|
-
throw new McpError(ErrorCode.MethodNotFound, `Tool ${name} not found`);
|
|
833
|
-
}
|
|
834
|
-
} catch (error) {
|
|
835
|
-
logger.error(`Error executing tool ${name}:`, error);
|
|
836
|
-
throw new McpError(ErrorCode.InternalError, `Failed to execute ${name}: ${error}`);
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
private async handleQueryIncidents(args: any) {
|
|
842
|
-
const { query, limit = 10, include__analysis = false, fields } = args;
|
|
843
|
-
|
|
844
|
-
logger.info(`Querying incidents with: ${query}`);
|
|
845
|
-
|
|
846
|
-
try {
|
|
847
|
-
// Convert natural language to ServiceNow query if needed
|
|
848
|
-
const processedQuery = this.processNaturalLanguageQuery(query, 'incident');
|
|
849
|
-
|
|
850
|
-
// Query incidents
|
|
851
|
-
const incidents = await this.client.searchRecords('incident', processedQuery, limit);
|
|
852
|
-
|
|
853
|
-
let result = {
|
|
854
|
-
total_results: incidents.success ? incidents.data.result.length : 0,
|
|
855
|
-
incidents: incidents.success ? incidents.data.result : []
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
// Add intelligent _analysis if requested
|
|
859
|
-
if (include__analysis && incidents.success && incidents.data.result.length > 0) {
|
|
860
|
-
const _analysis = await this.analyzeIncidents(incidents.data.result);
|
|
861
|
-
result = { ...result, ..._analysis };
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
return {
|
|
865
|
-
content: [
|
|
866
|
-
{
|
|
867
|
-
type: 'text',
|
|
868
|
-
text: `Found ${incidents.success ? incidents.data.result.length : 0} incidents matching query: "${query}"\n\n${JSON.stringify(result, null, 2)}`
|
|
869
|
-
}
|
|
870
|
-
]
|
|
871
|
-
};
|
|
872
|
-
} catch (error) {
|
|
873
|
-
logger.error('Error querying incidents:', error);
|
|
874
|
-
throw new McpError(ErrorCode.InternalError, `Failed to query incidents: ${error}`);
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
private async handleAnalyzeIncident(args: any) {
|
|
879
|
-
const { incident_id, include_similar = true, suggest_resolution = true } = args;
|
|
880
|
-
|
|
881
|
-
logger.info(`Analyzing incident: ${incident_id}`);
|
|
882
|
-
|
|
883
|
-
try {
|
|
884
|
-
// Get incident details
|
|
885
|
-
const incident = await this.getIncidentDetails(incident_id);
|
|
886
|
-
if (!incident) {
|
|
887
|
-
throw new Error(`Incident ${incident_id} not found`);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// Perform intelligent analysis
|
|
891
|
-
const _analysis = await this.performIncidentAnalysis(incident, include_similar, suggest_resolution);
|
|
892
|
-
|
|
893
|
-
return {
|
|
894
|
-
content: [
|
|
895
|
-
{
|
|
896
|
-
type: 'text',
|
|
897
|
-
text: `Incident Analysis for ${incident_id}:\n\n${JSON.stringify(_analysis, null, 2)}`
|
|
898
|
-
}
|
|
899
|
-
]
|
|
900
|
-
};
|
|
901
|
-
} catch (error) {
|
|
902
|
-
logger.error('Error analyzing incident:', error);
|
|
903
|
-
throw new McpError(ErrorCode.InternalError, `Failed to analyze incident: ${error}`);
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
private async handleAutoResolveIncident(args: any) {
|
|
908
|
-
const { incident_id, dry_run = true } = args;
|
|
909
|
-
|
|
910
|
-
logger.info(`Auto-resolving incident: ${incident_id} (dry_run: ${dry_run})`);
|
|
911
|
-
|
|
912
|
-
try {
|
|
913
|
-
// Get incident details
|
|
914
|
-
const incident = await this.getIncidentDetails(incident_id);
|
|
915
|
-
if (!incident) {
|
|
916
|
-
throw new Error(`Incident ${incident_id} not found`);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Analyze incident for auto-resolution potential
|
|
920
|
-
const _analysis = await this.performIncidentAnalysis(incident, true, true);
|
|
921
|
-
|
|
922
|
-
// Generate automated resolution actions
|
|
923
|
-
const resolutionActions = await this.generateResolutionActions(incident, _analysis);
|
|
924
|
-
|
|
925
|
-
const result: any = {
|
|
926
|
-
incident_id,
|
|
927
|
-
_analysis,
|
|
928
|
-
resolution_actions: resolutionActions,
|
|
929
|
-
dry_run,
|
|
930
|
-
actions_executed: []
|
|
931
|
-
};
|
|
932
|
-
|
|
933
|
-
// Execute actions if not dry run
|
|
934
|
-
if (!dry_run && resolutionActions.length > 0) {
|
|
935
|
-
result.actions_executed = await this.executeResolutionActions(incident_id, resolutionActions);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
return {
|
|
939
|
-
content: [
|
|
940
|
-
{
|
|
941
|
-
type: 'text',
|
|
942
|
-
text: `Auto-Resolution Analysis for ${incident_id}:\n\n${JSON.stringify(result, null, 2)}`
|
|
943
|
-
}
|
|
944
|
-
]
|
|
945
|
-
};
|
|
946
|
-
} catch (error) {
|
|
947
|
-
logger.error('Error auto-resolving incident:', error);
|
|
948
|
-
throw new McpError(ErrorCode.InternalError, `Failed to auto-resolve incident: ${error}`);
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
private async handleQueryRequests(args: any) {
|
|
953
|
-
const { query, limit = 10, include_items = false } = args;
|
|
954
|
-
|
|
955
|
-
logger.info(`Querying requests with: ${query}`);
|
|
956
|
-
|
|
957
|
-
try {
|
|
958
|
-
const processedQuery = this.processNaturalLanguageQuery(query, 'request');
|
|
959
|
-
const requests = await this.client.searchRecords('sc_request', processedQuery, limit);
|
|
960
|
-
|
|
961
|
-
let result: any = {
|
|
962
|
-
total_results: requests.success ? requests.data.result.length : 0,
|
|
963
|
-
requests: requests.success ? requests.data.result : []
|
|
964
|
-
};
|
|
965
|
-
|
|
966
|
-
// Include request items if requested
|
|
967
|
-
if (include_items && requests.success && requests.data.result.length > 0) {
|
|
968
|
-
const requestItems = await this.getRequestItems(requests.data.result);
|
|
969
|
-
result = { ...result, request_items: requestItems };
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
return {
|
|
973
|
-
content: [
|
|
974
|
-
{
|
|
975
|
-
type: 'text',
|
|
976
|
-
text: `Found ${requests.success ? requests.data.result.length : 0} requests matching query: "${query}"\n\n${JSON.stringify(result, null, 2)}`
|
|
977
|
-
}
|
|
978
|
-
]
|
|
979
|
-
};
|
|
980
|
-
} catch (error) {
|
|
981
|
-
logger.error('Error querying requests:', error);
|
|
982
|
-
throw new McpError(ErrorCode.InternalError, `Failed to query requests: ${error}`);
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
private async handleQueryProblems(args: any) {
|
|
987
|
-
const { query, limit = 10, include_incidents = false } = args;
|
|
988
|
-
|
|
989
|
-
logger.info(`Querying problems with: ${query}`);
|
|
990
|
-
|
|
991
|
-
try {
|
|
992
|
-
const processedQuery = this.processNaturalLanguageQuery(query, 'problem');
|
|
993
|
-
const problems = await this.client.searchRecords('problem', processedQuery, limit);
|
|
994
|
-
|
|
995
|
-
let result: any = {
|
|
996
|
-
total_results: problems.success ? problems.data.result.length : 0,
|
|
997
|
-
problems: problems.success ? problems.data.result : []
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
// Include related incidents if requested
|
|
1001
|
-
if (include_incidents && problems.success && problems.data.result.length > 0) {
|
|
1002
|
-
const incidents = await this.getRelatedIncidents(problems.data.result);
|
|
1003
|
-
result = { ...result, related_incidents: incidents };
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
return {
|
|
1007
|
-
content: [
|
|
1008
|
-
{
|
|
1009
|
-
type: 'text',
|
|
1010
|
-
text: `Found ${problems.success ? problems.data.result.length : 0} problems matching query: "${query}"\n\n${JSON.stringify(result, null, 2)}`
|
|
1011
|
-
}
|
|
1012
|
-
]
|
|
1013
|
-
};
|
|
1014
|
-
} catch (error) {
|
|
1015
|
-
logger.error('Error querying problems:', error);
|
|
1016
|
-
throw new McpError(ErrorCode.InternalError, `Failed to query problems: ${error}`);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
private async handleCMDBSearch(args: any) {
|
|
1021
|
-
const { query, ci_type = 'any', limit = 10, include_relationships = false } = args;
|
|
1022
|
-
|
|
1023
|
-
logger.info(`Searching CMDB with: ${query}, type: ${ci_type}`);
|
|
1024
|
-
|
|
1025
|
-
try {
|
|
1026
|
-
const ciTable = ci_type === 'any' ? 'cmdb_ci' : (operationalTableMapping as any)[ci_type] || 'cmdb_ci';
|
|
1027
|
-
const processedQuery = this.processNaturalLanguageQuery(query, 'cmdb');
|
|
1028
|
-
|
|
1029
|
-
const configItems = await this.client.searchRecords(ciTable, processedQuery, limit);
|
|
1030
|
-
|
|
1031
|
-
let result: any = {
|
|
1032
|
-
total_results: configItems.success ? configItems.data.result.length : 0,
|
|
1033
|
-
configuration_items: configItems.success ? configItems.data.result : [],
|
|
1034
|
-
ci_type: ci_type
|
|
1035
|
-
};
|
|
1036
|
-
|
|
1037
|
-
// Include relationships if requested
|
|
1038
|
-
if (include_relationships && configItems.success && configItems.data.result.length > 0) {
|
|
1039
|
-
const relationships = await this.getCIRelationships(configItems.data.result);
|
|
1040
|
-
result = { ...result, relationships };
|
|
1041
|
-
}
|
|
1042
|
-
|
|
1043
|
-
return {
|
|
1044
|
-
content: [
|
|
1045
|
-
{
|
|
1046
|
-
type: 'text',
|
|
1047
|
-
text: `Found ${configItems.success ? configItems.data.result.length : 0} configuration items matching query: "${query}"\n\n${JSON.stringify(result, null, 2)}`
|
|
1048
|
-
}
|
|
1049
|
-
]
|
|
1050
|
-
};
|
|
1051
|
-
} catch (error) {
|
|
1052
|
-
logger.error('Error searching CMDB:', error);
|
|
1053
|
-
throw new McpError(ErrorCode.InternalError, `Failed to search CMDB: ${error}`);
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
private async handleUserLookup(args: any) {
|
|
1058
|
-
const { identifier, include_roles = true, include_groups = true } = args;
|
|
1059
|
-
|
|
1060
|
-
logger.info(`Looking up user: ${identifier}`);
|
|
1061
|
-
|
|
1062
|
-
try {
|
|
1063
|
-
// Try different user lookup strategies
|
|
1064
|
-
let userQuery = '';
|
|
1065
|
-
if (identifier.includes('@')) {
|
|
1066
|
-
userQuery = `email=${identifier}`;
|
|
1067
|
-
} else if (identifier.includes('.')) {
|
|
1068
|
-
userQuery = `user_name=${identifier}`;
|
|
1069
|
-
} else {
|
|
1070
|
-
userQuery = `nameLIKE${identifier}^ORuser_nameLIKE${identifier}^ORemailLIKE${identifier}`;
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
const users = await this.client.searchRecords('sys_user', userQuery, 5);
|
|
1074
|
-
|
|
1075
|
-
if (!users.success || users.data.result.length === 0) {
|
|
1076
|
-
return {
|
|
1077
|
-
content: [
|
|
1078
|
-
{
|
|
1079
|
-
type: 'text',
|
|
1080
|
-
text: `No users found matching identifier: "${identifier}"`
|
|
1081
|
-
}
|
|
1082
|
-
]
|
|
1083
|
-
};
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
let result: any = {
|
|
1087
|
-
total_results: users.data.result.length,
|
|
1088
|
-
users: users.data.result
|
|
1089
|
-
};
|
|
1090
|
-
|
|
1091
|
-
// Include roles and groups if requested
|
|
1092
|
-
if (include_roles || include_groups) {
|
|
1093
|
-
const userDetails = await this.getUserDetails(users.data.result[0].sys_id, include_roles, include_groups);
|
|
1094
|
-
result = { ...result, user_details: userDetails };
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
return {
|
|
1098
|
-
content: [
|
|
1099
|
-
{
|
|
1100
|
-
type: 'text',
|
|
1101
|
-
text: `Found ${users.success ? users.data.result.length : 0} users matching identifier: "${identifier}"\n\n${JSON.stringify(result, null, 2)}`
|
|
1102
|
-
}
|
|
1103
|
-
]
|
|
1104
|
-
};
|
|
1105
|
-
} catch (error) {
|
|
1106
|
-
logger.error('Error looking up user:', error);
|
|
1107
|
-
throw new McpError(ErrorCode.InternalError, `Failed to lookup user: ${error}`);
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
private async handleOperationalMetrics(args: any) {
|
|
1112
|
-
const { timeframe = 'week', metric_types = [] } = args;
|
|
1113
|
-
|
|
1114
|
-
logger.info(`Getting operational metrics for timeframe: ${timeframe}`);
|
|
1115
|
-
|
|
1116
|
-
try {
|
|
1117
|
-
const metrics = await this.calculateOperationalMetrics(timeframe, metric_types);
|
|
1118
|
-
|
|
1119
|
-
return {
|
|
1120
|
-
content: [
|
|
1121
|
-
{
|
|
1122
|
-
type: 'text',
|
|
1123
|
-
text: `Operational Metrics (${timeframe}):\n\n${JSON.stringify(metrics, null, 2)}`
|
|
1124
|
-
}
|
|
1125
|
-
]
|
|
1126
|
-
};
|
|
1127
|
-
} catch (error) {
|
|
1128
|
-
logger.error('Error getting operational metrics:', error);
|
|
1129
|
-
throw new McpError(ErrorCode.InternalError, `Failed to get operational metrics: ${error}`);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
private async handlePatternAnalysis(args: any) {
|
|
1134
|
-
const { analysis_type, timeframe = 'week' } = args;
|
|
1135
|
-
|
|
1136
|
-
logger.info(`Performing pattern _analysis: ${analysis_type} for ${timeframe}`);
|
|
1137
|
-
|
|
1138
|
-
try {
|
|
1139
|
-
const patterns = await this.analyzePatterns(analysis_type, timeframe);
|
|
1140
|
-
|
|
1141
|
-
return {
|
|
1142
|
-
content: [
|
|
1143
|
-
{
|
|
1144
|
-
type: 'text',
|
|
1145
|
-
text: `Pattern Analysis (${analysis_type} - ${timeframe}):\n\n${JSON.stringify(patterns, null, 2)}`
|
|
1146
|
-
}
|
|
1147
|
-
]
|
|
1148
|
-
};
|
|
1149
|
-
} catch (error) {
|
|
1150
|
-
logger.error('Error analyzing patterns:', error);
|
|
1151
|
-
throw new McpError(ErrorCode.InternalError, `Failed to analyze patterns: ${error}`);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
private async handleKnowledgeSearch(args: any) {
|
|
1156
|
-
const { query, match_incident, limit = 5 } = args;
|
|
1157
|
-
|
|
1158
|
-
logger.info(`Searching knowledge base with: ${query}`);
|
|
1159
|
-
|
|
1160
|
-
try {
|
|
1161
|
-
const processedQuery = this.processNaturalLanguageQuery(query, 'knowledge');
|
|
1162
|
-
const articles = await this.client.searchRecords('kb_knowledge', processedQuery, limit);
|
|
1163
|
-
|
|
1164
|
-
let result: any = {
|
|
1165
|
-
total_results: articles.success ? articles.data.result.length : 0,
|
|
1166
|
-
knowledge_articles: articles.success ? articles.data.result : []
|
|
1167
|
-
};
|
|
1168
|
-
|
|
1169
|
-
// If matching to incident, provide relevance scoring
|
|
1170
|
-
if (match_incident) {
|
|
1171
|
-
const incident = await this.getIncidentDetails(match_incident);
|
|
1172
|
-
if (incident) {
|
|
1173
|
-
const relevanceScores = await this.calculateKnowledgeRelevance(articles.success ? articles.data.result : [], incident);
|
|
1174
|
-
result = { ...result, relevance_scores: relevanceScores };
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
return {
|
|
1179
|
-
content: [
|
|
1180
|
-
{
|
|
1181
|
-
type: 'text',
|
|
1182
|
-
text: `Found ${articles.success ? articles.data.result.length : 0} knowledge articles matching query: "${query}"\n\n${JSON.stringify(result, null, 2)}`
|
|
1183
|
-
}
|
|
1184
|
-
]
|
|
1185
|
-
};
|
|
1186
|
-
} catch (error) {
|
|
1187
|
-
logger.error('Error searching knowledge base:', error);
|
|
1188
|
-
throw new McpError(ErrorCode.InternalError, `Failed to search knowledge base: ${error}`);
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
private async handlePredictiveAnalysis(args: any) {
|
|
1193
|
-
const { prediction_type, timeframe = 'week' } = args;
|
|
1194
|
-
|
|
1195
|
-
logger.info(`Performing predictive _analysis: ${prediction_type} for ${timeframe}`);
|
|
1196
|
-
|
|
1197
|
-
try {
|
|
1198
|
-
const predictions = await this.performPredictiveAnalysis(prediction_type, timeframe);
|
|
1199
|
-
|
|
1200
|
-
return {
|
|
1201
|
-
content: [
|
|
1202
|
-
{
|
|
1203
|
-
type: 'text',
|
|
1204
|
-
text: `Predictive Analysis (${prediction_type} - ${timeframe}):\n\n${JSON.stringify(predictions, null, 2)}`
|
|
1205
|
-
}
|
|
1206
|
-
]
|
|
1207
|
-
};
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
logger.error('Error performing predictive _analysis:', error);
|
|
1210
|
-
throw new McpError(ErrorCode.InternalError, `Failed to perform predictive _analysis: ${error}`);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Helper methods for intelligent analysis
|
|
1215
|
-
|
|
1216
|
-
private processNaturalLanguageQuery(query: string, context: string): string {
|
|
1217
|
-
// Convert natural language to ServiceNow encoded query
|
|
1218
|
-
const lowerQuery = query.toLowerCase();
|
|
1219
|
-
|
|
1220
|
-
// Common ServiceNow query patterns
|
|
1221
|
-
if (lowerQuery.includes('high priority')) {
|
|
1222
|
-
return 'priority=1';
|
|
1223
|
-
}
|
|
1224
|
-
if (lowerQuery.includes('critical')) {
|
|
1225
|
-
return 'priority=1^ORseverity=1';
|
|
1226
|
-
}
|
|
1227
|
-
if (lowerQuery.includes('open') || lowerQuery.includes('active')) {
|
|
1228
|
-
return 'active=true^state!=6^state!=7';
|
|
1229
|
-
}
|
|
1230
|
-
if (lowerQuery.includes('closed') || lowerQuery.includes('resolved')) {
|
|
1231
|
-
return 'state=6^ORstate=7';
|
|
1232
|
-
}
|
|
1233
|
-
if (lowerQuery.includes('today')) {
|
|
1234
|
-
return 'sys_created_onONToday@javascript:gs.daysAgoStart(0)@javascript:gs.daysAgoEnd(0)';
|
|
1235
|
-
}
|
|
1236
|
-
if (lowerQuery.includes('last week')) {
|
|
1237
|
-
return 'sys_created_onONLast week@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(1)';
|
|
1238
|
-
}
|
|
1239
|
-
|
|
1240
|
-
// Context-specific processing
|
|
1241
|
-
if (context === 'incident') {
|
|
1242
|
-
if (lowerQuery.includes('network')) {
|
|
1243
|
-
return 'categoryLIKEnetwork^ORshort_descriptionLIKEnetwork';
|
|
1244
|
-
}
|
|
1245
|
-
if (lowerQuery.includes('database')) {
|
|
1246
|
-
return 'categoryLIKEdatabase^ORshort_descriptionLIKEdatabase';
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
// Default: treat as general search
|
|
1251
|
-
return `short_descriptionLIKE${query}^ORdescriptionLIKE${query}`;
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
private async analyzeIncidents(incidents: any[]) {
|
|
1255
|
-
const patterns = [];
|
|
1256
|
-
const categories = new Map();
|
|
1257
|
-
|
|
1258
|
-
for (const incident of incidents) {
|
|
1259
|
-
// Count categories
|
|
1260
|
-
const category = incident.category || 'uncategorized';
|
|
1261
|
-
categories.set(category, (categories.get(category) || 0) + 1);
|
|
1262
|
-
|
|
1263
|
-
// Detect patterns
|
|
1264
|
-
const description = (incident.short_description || '').toLowerCase();
|
|
1265
|
-
for (const [patternName, pattern] of Object.entries(commonPatterns)) {
|
|
1266
|
-
if (pattern.keywords.some(keyword => description.includes(keyword))) {
|
|
1267
|
-
patterns.push({
|
|
1268
|
-
incident_number: incident.number,
|
|
1269
|
-
pattern: patternName,
|
|
1270
|
-
confidence: 0.8
|
|
1271
|
-
});
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
return {
|
|
1277
|
-
patterns_detected: patterns,
|
|
1278
|
-
category_distribution: Object.fromEntries(categories),
|
|
1279
|
-
total_analyzed: incidents.length
|
|
1280
|
-
};
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
private async getIncidentDetails(incident_id: string): Promise<any> {
|
|
1284
|
-
// Try to get incident by number first, then by sys_id
|
|
1285
|
-
let query = `number=${incident_id}`;
|
|
1286
|
-
let incidents = await this.client.searchRecords('incident', query, 1);
|
|
1287
|
-
|
|
1288
|
-
if (!incidents.success || incidents.data.result.length === 0) {
|
|
1289
|
-
// Try by sys_id
|
|
1290
|
-
query = `sys_id=${incident_id}`;
|
|
1291
|
-
incidents = await this.client.searchRecords('incident', query, 1);
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
return (incidents.success && incidents.data.result.length > 0) ? incidents.data.result[0] : null;
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
private async performIncidentAnalysis(incident: any, include_similar: boolean, suggest_resolution: boolean): Promise<IncidentAnalysis> {
|
|
1298
|
-
const _analysis: IncidentAnalysis = {
|
|
1299
|
-
incident_id: incident.number,
|
|
1300
|
-
patterns_found: [],
|
|
1301
|
-
root_cause__analysis: '',
|
|
1302
|
-
suggested_resolution: [],
|
|
1303
|
-
confidence_score: 0,
|
|
1304
|
-
similar_incidents: [],
|
|
1305
|
-
knowledge_articles: [],
|
|
1306
|
-
automated_actions: []
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
|
-
// Pattern detection
|
|
1310
|
-
const description = (incident.short_description || '').toLowerCase();
|
|
1311
|
-
for (const [patternName, pattern] of Object.entries(commonPatterns)) {
|
|
1312
|
-
if (pattern.keywords.some(keyword => description.includes(keyword))) {
|
|
1313
|
-
_analysis.patterns_found.push(patternName);
|
|
1314
|
-
_analysis.suggested_resolution.push(...pattern.common_solutions);
|
|
1315
|
-
_analysis.confidence_score += 0.2;
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
// Find similar incidents
|
|
1320
|
-
if (include_similar) {
|
|
1321
|
-
const similarQuery = `short_descriptionLIKE${incident.short_description}^sys_id!=${incident.sys_id}^state=6`;
|
|
1322
|
-
const similarIncidents = await this.client.searchRecords('incident', similarQuery, 5);
|
|
1323
|
-
_analysis.similar_incidents = similarIncidents.success ? similarIncidents.data.result : [];
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
// Root cause analysis
|
|
1327
|
-
_analysis.root_cause__analysis = this.generateRootCauseAnalysis(incident, _analysis.patterns_found);
|
|
1328
|
-
|
|
1329
|
-
// Search knowledge base
|
|
1330
|
-
const kbQuery = `textLIKE${incident.short_description}^workflow_state=published`;
|
|
1331
|
-
const kbArticles = await this.client.searchRecords('kb_knowledge', kbQuery, 3);
|
|
1332
|
-
_analysis.knowledge_articles = kbArticles.success ? kbArticles.data.result : [];
|
|
1333
|
-
|
|
1334
|
-
// Automated actions
|
|
1335
|
-
_analysis.automated_actions = this.generateAutomatedActions(incident, _analysis.patterns_found);
|
|
1336
|
-
|
|
1337
|
-
return _analysis;
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
private generateRootCauseAnalysis(incident: any, patterns: string[]): string {
|
|
1341
|
-
if (patterns.length === 0) {
|
|
1342
|
-
return 'No specific patterns detected. Manual investigation required.';
|
|
1343
|
-
}
|
|
1344
|
-
|
|
1345
|
-
const _analysis = [];
|
|
1346
|
-
|
|
1347
|
-
if (patterns.includes('network_issues')) {
|
|
1348
|
-
_analysis.push('Network connectivity issue detected. Check network infrastructure, DNS, and firewall rules.');
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
if (patterns.includes('database_issues')) {
|
|
1352
|
-
_analysis.push('Database connectivity or performance issue detected. Check database server, connection pools, and query performance.');
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
if (patterns.includes('application_errors')) {
|
|
1356
|
-
_analysis.push('Application error detected. Check application logs, resource utilization, and service status.');
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (patterns.includes('auth_issues')) {
|
|
1360
|
-
_analysis.push('Authentication issue detected. Check user credentials, LDAP/SSO configuration, and permissions.');
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
return _analysis.join(' ');
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
private generateAutomatedActions(incident: any, patterns: string[]): string[] {
|
|
1367
|
-
const actions: string[] = [];
|
|
1368
|
-
|
|
1369
|
-
if (patterns.includes('network_issues')) {
|
|
1370
|
-
actions.push('Execute network connectivity test');
|
|
1371
|
-
actions.push('Check DNS resolution');
|
|
1372
|
-
actions.push('Verify firewall rules');
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
if (patterns.includes('database_issues')) {
|
|
1376
|
-
actions.push('Test database connectivity');
|
|
1377
|
-
actions.push('Check database performance metrics');
|
|
1378
|
-
actions.push('Review connection pool status');
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
if (patterns.includes('application_errors')) {
|
|
1382
|
-
actions.push('Restart application service');
|
|
1383
|
-
actions.push('Check application logs');
|
|
1384
|
-
actions.push('Monitor resource utilization');
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (patterns.includes('auth_issues')) {
|
|
1388
|
-
actions.push('Reset user session');
|
|
1389
|
-
actions.push('Verify user permissions');
|
|
1390
|
-
actions.push('Check authentication service status');
|
|
1391
|
-
}
|
|
1392
|
-
|
|
1393
|
-
return actions;
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
private async generateResolutionActions(incident: any, _analysis: IncidentAnalysis): Promise<string[]> {
|
|
1397
|
-
const actions: string[] = [];
|
|
1398
|
-
|
|
1399
|
-
// Add high-confidence automated actions
|
|
1400
|
-
if (_analysis.confidence_score > 0.6) {
|
|
1401
|
-
actions.push(..._analysis.automated_actions);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
// Add knowledge-based actions
|
|
1405
|
-
if (_analysis.knowledge_articles.length > 0) {
|
|
1406
|
-
actions.push('Apply resolution from knowledge article');
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
// Add pattern-based actions
|
|
1410
|
-
if (_analysis.similar_incidents.length > 0) {
|
|
1411
|
-
actions.push('Apply resolution from similar incident');
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
return actions;
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
private async executeResolutionActions(incident_id: string, actions: string[]): Promise<string[]> {
|
|
1418
|
-
const executed = [];
|
|
1419
|
-
|
|
1420
|
-
for (const action of actions) {
|
|
1421
|
-
try {
|
|
1422
|
-
// This would integrate with actual automation systems
|
|
1423
|
-
logger.info(`Executing action: ${action} for incident ${incident_id}`);
|
|
1424
|
-
executed.push(`✅ ${action}`);
|
|
1425
|
-
} catch (error) {
|
|
1426
|
-
executed.push(`❌ ${action}: ${error}`);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
return executed;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
private async getRequestItems(requests: any[]): Promise<any[]> {
|
|
1434
|
-
const items = [];
|
|
1435
|
-
|
|
1436
|
-
for (const request of requests) {
|
|
1437
|
-
const requestItems = await this.client.searchRecords('sc_req_item', `request=${request.sys_id}`, 10);
|
|
1438
|
-
if (requestItems.success) {
|
|
1439
|
-
items.push(...requestItems.data.result);
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
return items;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
private async getRelatedIncidents(problems: any[]): Promise<any[]> {
|
|
1447
|
-
const incidents = [];
|
|
1448
|
-
|
|
1449
|
-
for (const problem of problems) {
|
|
1450
|
-
const relatedIncidents = await this.client.searchRecords('incident', `problem_id=${problem.sys_id}`, 10);
|
|
1451
|
-
if (relatedIncidents.success) {
|
|
1452
|
-
incidents.push(...relatedIncidents.data.result);
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
return incidents;
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
private async getCIRelationships(configItems: any[]): Promise<any[]> {
|
|
1460
|
-
const relationships = [];
|
|
1461
|
-
|
|
1462
|
-
for (const ci of configItems) {
|
|
1463
|
-
try {
|
|
1464
|
-
const ciRelationships = await this.client.searchRecords('cmdb_rel_ci', `parent=${ci.sys_id}^ORchild=${ci.sys_id}`, 10);
|
|
1465
|
-
relationships.push({
|
|
1466
|
-
ci_id: ci.sys_id,
|
|
1467
|
-
ci_name: ci.name,
|
|
1468
|
-
relationships: ciRelationships
|
|
1469
|
-
});
|
|
1470
|
-
} catch (error) {
|
|
1471
|
-
logger.error(`Error getting relationships for CI ${ci.sys_id}:`, error);
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
return relationships;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
private async getUserDetails(userId: string, include_roles: boolean, include_groups: boolean): Promise<any> {
|
|
1479
|
-
const details: any = {};
|
|
1480
|
-
|
|
1481
|
-
if (include_roles) {
|
|
1482
|
-
details.roles = await this.client.searchRecords('sys_user_has_role', `user=${userId}`, 20);
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
if (include_groups) {
|
|
1486
|
-
details.groups = await this.client.searchRecords('sys_user_grmember', `user=${userId}`, 20);
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
return details;
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
private async calculateOperationalMetrics(timeframe: string, metricTypes: string[]): Promise<OperationalMetrics> {
|
|
1493
|
-
const dateFilter = this.getDateFilter(timeframe);
|
|
1494
|
-
|
|
1495
|
-
const metrics: OperationalMetrics = {
|
|
1496
|
-
total_incidents: 0,
|
|
1497
|
-
open_incidents: 0,
|
|
1498
|
-
high_priority_incidents: 0,
|
|
1499
|
-
avg_resolution_time: 0,
|
|
1500
|
-
common_categories: [],
|
|
1501
|
-
trending_issues: []
|
|
1502
|
-
};
|
|
1503
|
-
|
|
1504
|
-
try {
|
|
1505
|
-
// Get total incidents
|
|
1506
|
-
const totalIncidents = await this.client.searchRecords('incident', `sys_created_on${dateFilter}`, 1000);
|
|
1507
|
-
metrics.total_incidents = totalIncidents.success ? totalIncidents.data.result.length : 0;
|
|
1508
|
-
|
|
1509
|
-
// Get open incidents
|
|
1510
|
-
const openIncidents = await this.client.searchRecords('incident', `active=true^sys_created_on${dateFilter}`, 1000);
|
|
1511
|
-
metrics.open_incidents = openIncidents.success ? openIncidents.data.result.length : 0;
|
|
1512
|
-
|
|
1513
|
-
// Get high priority incidents
|
|
1514
|
-
const highPriorityIncidents = await this.client.searchRecords('incident', `priority=1^sys_created_on${dateFilter}`, 1000);
|
|
1515
|
-
metrics.high_priority_incidents = highPriorityIncidents.success ? highPriorityIncidents.data.result.length : 0;
|
|
1516
|
-
|
|
1517
|
-
// Calculate common categories
|
|
1518
|
-
const categories = new Map();
|
|
1519
|
-
if (totalIncidents.success) {
|
|
1520
|
-
totalIncidents.data.result.forEach((incident: any) => {
|
|
1521
|
-
const category = incident.category || 'uncategorized';
|
|
1522
|
-
categories.set(category, (categories.get(category) || 0) + 1);
|
|
1523
|
-
});
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
metrics.common_categories = Array.from(categories.entries())
|
|
1527
|
-
.sort((a, b) => b[1] - a[1])
|
|
1528
|
-
.slice(0, 5)
|
|
1529
|
-
.map(([category, count]) => `${category}: ${count}`);
|
|
1530
|
-
|
|
1531
|
-
} catch (error) {
|
|
1532
|
-
logger.error('Error calculating operational metrics:', error);
|
|
1533
|
-
}
|
|
1534
|
-
|
|
1535
|
-
return metrics;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
private getDateFilter(timeframe: string): string {
|
|
1539
|
-
switch (timeframe) {
|
|
1540
|
-
case 'today':
|
|
1541
|
-
return 'ONToday@javascript:gs.daysAgoStart(0)@javascript:gs.daysAgoEnd(0)';
|
|
1542
|
-
case 'week':
|
|
1543
|
-
return 'ONLast 7 days@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)';
|
|
1544
|
-
case 'month':
|
|
1545
|
-
return 'ONLast 30 days@javascript:gs.daysAgoStart(30)@javascript:gs.daysAgoEnd(0)';
|
|
1546
|
-
case 'quarter':
|
|
1547
|
-
return 'ONLast 90 days@javascript:gs.daysAgoStart(90)@javascript:gs.daysAgoEnd(0)';
|
|
1548
|
-
default:
|
|
1549
|
-
return 'ONLast 7 days@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)';
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
private async analyzePatterns(analysisType: string, timeframe: string): Promise<any> {
|
|
1554
|
-
const dateFilter = this.getDateFilter(timeframe);
|
|
1555
|
-
|
|
1556
|
-
switch (analysisType) {
|
|
1557
|
-
case 'incident_patterns':
|
|
1558
|
-
return await this.analyzeIncidentPatterns(dateFilter);
|
|
1559
|
-
case 'request_trends':
|
|
1560
|
-
return await this.analyzeRequestTrends(dateFilter);
|
|
1561
|
-
case 'problem_root_causes':
|
|
1562
|
-
return await this.analyzeProblemRootCauses(dateFilter);
|
|
1563
|
-
case 'user_behavior':
|
|
1564
|
-
return await this.analyzeUserBehavior(dateFilter);
|
|
1565
|
-
default:
|
|
1566
|
-
return { error: 'Unknown _analysis type' };
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
private async analyzeIncidentPatterns(dateFilter: string): Promise<any> {
|
|
1571
|
-
const incidents = await this.client.searchRecords('incident', `sys_created_on${dateFilter}`, 1000);
|
|
1572
|
-
|
|
1573
|
-
const patterns = {
|
|
1574
|
-
by_category: new Map(),
|
|
1575
|
-
by_priority: new Map(),
|
|
1576
|
-
by_state: new Map(),
|
|
1577
|
-
by_hour: new Map(),
|
|
1578
|
-
common_keywords: new Map()
|
|
1579
|
-
};
|
|
1580
|
-
|
|
1581
|
-
if (incidents.success) {
|
|
1582
|
-
incidents.data.result.forEach((incident: any) => {
|
|
1583
|
-
// Category patterns
|
|
1584
|
-
const category = incident.category || 'uncategorized';
|
|
1585
|
-
patterns.by_category.set(category, (patterns.by_category.get(category) || 0) + 1);
|
|
1586
|
-
|
|
1587
|
-
// Priority patterns
|
|
1588
|
-
const priority = incident.priority || 'unknown';
|
|
1589
|
-
patterns.by_priority.set(priority, (patterns.by_priority.get(priority) || 0) + 1);
|
|
1590
|
-
|
|
1591
|
-
// State patterns
|
|
1592
|
-
const state = incident.state || 'unknown';
|
|
1593
|
-
patterns.by_state.set(state, (patterns.by_state.get(state) || 0) + 1);
|
|
1594
|
-
|
|
1595
|
-
// Time patterns
|
|
1596
|
-
if (incident.sys_created_on) {
|
|
1597
|
-
const hour = new Date(incident.sys_created_on).getHours();
|
|
1598
|
-
patterns.by_hour.set(hour, (patterns.by_hour.get(hour) || 0) + 1);
|
|
1599
|
-
}
|
|
1600
|
-
|
|
1601
|
-
// Keyword patterns
|
|
1602
|
-
const description = (incident.short_description || '').toLowerCase();
|
|
1603
|
-
const keywords = description.split(/\s+/).filter((word: string) => word.length > 3);
|
|
1604
|
-
keywords.forEach((keyword: string) => {
|
|
1605
|
-
patterns.common_keywords.set(keyword, (patterns.common_keywords.get(keyword) || 0) + 1);
|
|
1606
|
-
});
|
|
1607
|
-
});
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
return {
|
|
1611
|
-
total_incidents: incidents.success ? incidents.data.result.length : 0,
|
|
1612
|
-
patterns: {
|
|
1613
|
-
by_category: Object.fromEntries(patterns.by_category),
|
|
1614
|
-
by_priority: Object.fromEntries(patterns.by_priority),
|
|
1615
|
-
by_state: Object.fromEntries(patterns.by_state),
|
|
1616
|
-
by_hour: Object.fromEntries(patterns.by_hour),
|
|
1617
|
-
top_keywords: Array.from(patterns.common_keywords.entries())
|
|
1618
|
-
.sort((a, b) => b[1] - a[1])
|
|
1619
|
-
.slice(0, 10)
|
|
1620
|
-
.map(([keyword, count]) => ({ keyword, count }))
|
|
1621
|
-
}
|
|
1622
|
-
};
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
private async analyzeRequestTrends(dateFilter: string): Promise<any> {
|
|
1626
|
-
const requests = await this.client.searchRecords('sc_request', `sys_created_on${dateFilter}`, 1000);
|
|
1627
|
-
|
|
1628
|
-
const trends = {
|
|
1629
|
-
by_state: new Map(),
|
|
1630
|
-
by_requested_for: new Map(),
|
|
1631
|
-
by_approval_status: new Map()
|
|
1632
|
-
};
|
|
1633
|
-
|
|
1634
|
-
if (requests.success) {
|
|
1635
|
-
requests.data.result.forEach((request: any) => {
|
|
1636
|
-
const state = request.state || 'unknown';
|
|
1637
|
-
trends.by_state.set(state, (trends.by_state.get(state) || 0) + 1);
|
|
1638
|
-
|
|
1639
|
-
const requestedFor = request.requested_for || 'unknown';
|
|
1640
|
-
trends.by_requested_for.set(requestedFor, (trends.by_requested_for.get(requestedFor) || 0) + 1);
|
|
1641
|
-
|
|
1642
|
-
const approvalStatus = request.approval || 'unknown';
|
|
1643
|
-
trends.by_approval_status.set(approvalStatus, (trends.by_approval_status.get(approvalStatus) || 0) + 1);
|
|
1644
|
-
});
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
return {
|
|
1648
|
-
total_requests: requests.success ? requests.data.result.length : 0,
|
|
1649
|
-
trends: {
|
|
1650
|
-
by_state: Object.fromEntries(trends.by_state),
|
|
1651
|
-
top_requesters: Array.from(trends.by_requested_for.entries())
|
|
1652
|
-
.sort((a, b) => b[1] - a[1])
|
|
1653
|
-
.slice(0, 10),
|
|
1654
|
-
by_approval_status: Object.fromEntries(trends.by_approval_status)
|
|
1655
|
-
}
|
|
1656
|
-
};
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
private async analyzeProblemRootCauses(dateFilter: string): Promise<any> {
|
|
1660
|
-
const problems = await this.client.searchRecords('problem', `sys_created_on${dateFilter}`, 1000);
|
|
1661
|
-
|
|
1662
|
-
const rootCauses = {
|
|
1663
|
-
by_category: new Map(),
|
|
1664
|
-
by_state: new Map(),
|
|
1665
|
-
resolution_patterns: new Map()
|
|
1666
|
-
};
|
|
1667
|
-
|
|
1668
|
-
if (problems.success) {
|
|
1669
|
-
problems.data.result.forEach((problem: any) => {
|
|
1670
|
-
const category = problem.category || 'uncategorized';
|
|
1671
|
-
rootCauses.by_category.set(category, (rootCauses.by_category.get(category) || 0) + 1);
|
|
1672
|
-
|
|
1673
|
-
const state = problem.state || 'unknown';
|
|
1674
|
-
rootCauses.by_state.set(state, (rootCauses.by_state.get(state) || 0) + 1);
|
|
1675
|
-
|
|
1676
|
-
if (problem.resolution_notes) {
|
|
1677
|
-
const resolution = problem.resolution_notes.toLowerCase();
|
|
1678
|
-
// Simple pattern matching for resolution types
|
|
1679
|
-
if (resolution.includes('restart')) {
|
|
1680
|
-
rootCauses.resolution_patterns.set('restart', (rootCauses.resolution_patterns.get('restart') || 0) + 1);
|
|
1681
|
-
}
|
|
1682
|
-
if (resolution.includes('configuration')) {
|
|
1683
|
-
rootCauses.resolution_patterns.set('configuration', (rootCauses.resolution_patterns.get('configuration') || 0) + 1);
|
|
1684
|
-
}
|
|
1685
|
-
if (resolution.includes('patch') || resolution.includes('update')) {
|
|
1686
|
-
rootCauses.resolution_patterns.set('patch/update', (rootCauses.resolution_patterns.get('patch/update') || 0) + 1);
|
|
1687
|
-
}
|
|
1688
|
-
}
|
|
1689
|
-
});
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
return {
|
|
1693
|
-
total_problems: problems.success ? problems.data.result.length : 0,
|
|
1694
|
-
root_causes: {
|
|
1695
|
-
by_category: Object.fromEntries(rootCauses.by_category),
|
|
1696
|
-
by_state: Object.fromEntries(rootCauses.by_state),
|
|
1697
|
-
resolution_patterns: Object.fromEntries(rootCauses.resolution_patterns)
|
|
1698
|
-
}
|
|
1699
|
-
};
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
private async analyzeUserBehavior(dateFilter: string): Promise<any> {
|
|
1703
|
-
try {
|
|
1704
|
-
// Query actual user activity data from ServiceNow
|
|
1705
|
-
const activities = [];
|
|
1706
|
-
|
|
1707
|
-
// Get recent incidents created by users
|
|
1708
|
-
const incidents = await this.client.searchRecords(
|
|
1709
|
-
'incident',
|
|
1710
|
-
`sys_created_on>=${dateFilter}^caller_idISNOTEMPTY`,
|
|
1711
|
-
100
|
|
1712
|
-
);
|
|
1713
|
-
|
|
1714
|
-
if (incidents.success && incidents.data) {
|
|
1715
|
-
const incidentData = Array.isArray(incidents.data) ? incidents.data : [incidents.data];
|
|
1716
|
-
const userIncidents = new Map();
|
|
1717
|
-
|
|
1718
|
-
incidentData.forEach(inc => {
|
|
1719
|
-
const userId = inc.caller_id?.value || inc.caller_id;
|
|
1720
|
-
if (userId) {
|
|
1721
|
-
userIncidents.set(userId, (userIncidents.get(userId) || 0) + 1);
|
|
1722
|
-
}
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
activities.push({
|
|
1726
|
-
metric: 'incident_reporting',
|
|
1727
|
-
total_incidents: incidentData.length,
|
|
1728
|
-
unique_users: userIncidents.size,
|
|
1729
|
-
avg_per_user: userIncidents.size > 0 ? Math.round(incidentData.length / userIncidents.size * 10) / 10 : 0
|
|
1730
|
-
});
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// Get service requests
|
|
1734
|
-
const requests = await this.client.searchRecords(
|
|
1735
|
-
'sc_request',
|
|
1736
|
-
`sys_created_on>=${dateFilter}^requested_forISNOTEMPTY`,
|
|
1737
|
-
100
|
|
1738
|
-
);
|
|
1739
|
-
|
|
1740
|
-
if (requests.success && requests.data) {
|
|
1741
|
-
const requestData = Array.isArray(requests.data) ? requests.data : [requests.data];
|
|
1742
|
-
activities.push({
|
|
1743
|
-
metric: 'request_frequency',
|
|
1744
|
-
total_requests: requestData.length,
|
|
1745
|
-
period: dateFilter
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
return {
|
|
1750
|
-
analysis_period: dateFilter,
|
|
1751
|
-
generated_at: new Date().toISOString(),
|
|
1752
|
-
user_activities: activities,
|
|
1753
|
-
summary: `Analyzed ${activities.length} activity metrics for period: ${dateFilter}`,
|
|
1754
|
-
note: 'Analysis based on actual ServiceNow data from incident and request tables'
|
|
1755
|
-
};
|
|
1756
|
-
|
|
1757
|
-
} catch (error) {
|
|
1758
|
-
return {
|
|
1759
|
-
error: 'User behavior _analysis failed',
|
|
1760
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1761
|
-
available_metrics: ['incident_reporting', 'request_frequency']
|
|
1762
|
-
};
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
private async calculateKnowledgeRelevance(articles: any[], incident: any): Promise<any[]> {
|
|
1767
|
-
const relevanceScores: any[] = [];
|
|
1768
|
-
|
|
1769
|
-
const incidentDescription = (incident.short_description || '').toLowerCase();
|
|
1770
|
-
const incidentKeywords = incidentDescription.split(/\s+/).filter((word: string) => word.length > 3);
|
|
1771
|
-
|
|
1772
|
-
articles.forEach(article => {
|
|
1773
|
-
const articleText = (article.text || '').toLowerCase();
|
|
1774
|
-
const articleKeywords = articleText.split(/\s+/).filter((word: string) => word.length > 3);
|
|
1775
|
-
|
|
1776
|
-
// Simple relevance scoring based on keyword overlap
|
|
1777
|
-
const commonKeywords = incidentKeywords.filter((keyword: string) => articleKeywords.includes(keyword));
|
|
1778
|
-
const relevanceScore = commonKeywords.length / Math.max(incidentKeywords.length, 1);
|
|
1779
|
-
|
|
1780
|
-
relevanceScores.push({
|
|
1781
|
-
article_id: article.sys_id,
|
|
1782
|
-
article_title: article.title,
|
|
1783
|
-
relevance_score: relevanceScore,
|
|
1784
|
-
common_keywords: commonKeywords
|
|
1785
|
-
});
|
|
1786
|
-
});
|
|
1787
|
-
|
|
1788
|
-
return relevanceScores.sort((a: any, b: any) => b.relevance_score - a.relevance_score);
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
private async performPredictiveAnalysis(predictionType: string, timeframe: string): Promise<any> {
|
|
1792
|
-
try {
|
|
1793
|
-
const currentDate = new Date();
|
|
1794
|
-
const _analysis: any = {
|
|
1795
|
-
prediction_type: predictionType,
|
|
1796
|
-
timeframe: timeframe,
|
|
1797
|
-
generated_at: currentDate.toISOString(),
|
|
1798
|
-
analysis_method: 'trend__analysis',
|
|
1799
|
-
data_points: []
|
|
1800
|
-
};
|
|
1801
|
-
|
|
1802
|
-
switch (predictionType) {
|
|
1803
|
-
case 'incident_volume':
|
|
1804
|
-
_analysis.data_points = await this.analyzeIncidentTrends(timeframe);
|
|
1805
|
-
_analysis.interpretation = 'Based on recent incident creation patterns';
|
|
1806
|
-
break;
|
|
1807
|
-
case 'system_failure':
|
|
1808
|
-
_analysis.data_points = await this.analyzeSystemHealthTrends(timeframe);
|
|
1809
|
-
_analysis.interpretation = 'Based on critical incident patterns and system health indicators';
|
|
1810
|
-
break;
|
|
1811
|
-
case 'resource_exhaustion':
|
|
1812
|
-
_analysis.data_points = await this.analyzeResourceTrends(timeframe);
|
|
1813
|
-
_analysis.interpretation = 'Based on request volumes and capacity indicators';
|
|
1814
|
-
break;
|
|
1815
|
-
case 'user_impact':
|
|
1816
|
-
_analysis.data_points = await this.analyzeUserImpactTrends(timeframe);
|
|
1817
|
-
_analysis.interpretation = 'Based on user-reported incidents and service requests';
|
|
1818
|
-
break;
|
|
1819
|
-
default:
|
|
1820
|
-
_analysis.data_points = [{ error: 'Unknown prediction type', supported_types: ['incident_volume', 'system_failure', 'resource_exhaustion', 'user_impact'] }];
|
|
1821
|
-
_analysis.interpretation = 'Unsupported _analysis type';
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
return _analysis;
|
|
1825
|
-
} catch (error) {
|
|
1826
|
-
return {
|
|
1827
|
-
prediction_type: predictionType,
|
|
1828
|
-
timeframe: timeframe,
|
|
1829
|
-
error: 'Analysis failed',
|
|
1830
|
-
message: error instanceof Error ? error.message : String(error),
|
|
1831
|
-
generated_at: new Date().toISOString()
|
|
1832
|
-
};
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
// New trend _analysis methods
|
|
1837
|
-
private async analyzeIncidentTrends(timeframe: string): Promise<any[]> {
|
|
1838
|
-
try {
|
|
1839
|
-
const incidents = await this.client.searchRecords('incident', 'sys_created_onONToday@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)', 100);
|
|
1840
|
-
|
|
1841
|
-
if (incidents.success && incidents.data) {
|
|
1842
|
-
const incidentData = Array.isArray(incidents.data) ? incidents.data : [incidents.data];
|
|
1843
|
-
const dailyCounts = new Map();
|
|
1844
|
-
|
|
1845
|
-
incidentData.forEach(incident => {
|
|
1846
|
-
const date = new Date(incident.sys_created_on).toDateString();
|
|
1847
|
-
dailyCounts.set(date, (dailyCounts.get(date) || 0) + 1);
|
|
1848
|
-
});
|
|
1849
|
-
|
|
1850
|
-
return Array.from(dailyCounts.entries()).map(([date, count]) => ({
|
|
1851
|
-
date,
|
|
1852
|
-
incident_count: count,
|
|
1853
|
-
trend: count > 5 ? 'increasing' : 'stable'
|
|
1854
|
-
}));
|
|
1855
|
-
}
|
|
1856
|
-
} catch (error) {
|
|
1857
|
-
return [{ error: 'Failed to analyze incident trends', message: String(error) }];
|
|
1858
|
-
}
|
|
1859
|
-
return [{ message: 'No incident data available for _analysis' }];
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
private async analyzeSystemHealthTrends(timeframe: string): Promise<any[]> {
|
|
1863
|
-
try {
|
|
1864
|
-
const criticalIncidents = await this.client.searchRecords('incident', 'priority=1^sys_created_onONToday@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)', 50);
|
|
1865
|
-
|
|
1866
|
-
if (criticalIncidents.success && criticalIncidents.data) {
|
|
1867
|
-
const criticalData = Array.isArray(criticalIncidents.data) ? criticalIncidents.data : [criticalIncidents.data];
|
|
1868
|
-
|
|
1869
|
-
return [{
|
|
1870
|
-
metric: 'critical_incidents',
|
|
1871
|
-
count: criticalData.length,
|
|
1872
|
-
health_indicator: criticalData.length > 3 ? 'at_risk' : 'stable',
|
|
1873
|
-
recent_issues: criticalData.slice(0, 3).map(inc => inc.short_description)
|
|
1874
|
-
}];
|
|
1875
|
-
}
|
|
1876
|
-
} catch (error) {
|
|
1877
|
-
return [{ error: 'Failed to analyze system health trends', message: String(error) }];
|
|
1878
|
-
}
|
|
1879
|
-
return [{ message: 'No system health data available' }];
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
private async analyzeResourceTrends(timeframe: string): Promise<any[]> {
|
|
1883
|
-
try {
|
|
1884
|
-
const requests = await this.client.searchRecords('sc_request', 'sys_created_onONToday@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)', 100);
|
|
1885
|
-
|
|
1886
|
-
if (requests.success && requests.data) {
|
|
1887
|
-
const requestData = Array.isArray(requests.data) ? requests.data : [requests.data];
|
|
1888
|
-
|
|
1889
|
-
return [{
|
|
1890
|
-
metric: 'request_volume',
|
|
1891
|
-
total_requests: requestData.length,
|
|
1892
|
-
resource_pressure: requestData.length > 20 ? 'high' : 'normal',
|
|
1893
|
-
trend: 'stable'
|
|
1894
|
-
}];
|
|
1895
|
-
}
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
return [{ error: 'Failed to analyze resource trends', message: String(error) }];
|
|
1898
|
-
}
|
|
1899
|
-
return [{ message: 'No resource usage data available' }];
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
private async analyzeUserImpactTrends(timeframe: string): Promise<any[]> {
|
|
1903
|
-
try {
|
|
1904
|
-
const userIncidents = await this.client.searchRecords('incident', 'caller_idISNOTEMPTY^sys_created_onONToday@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)', 100);
|
|
1905
|
-
|
|
1906
|
-
if (userIncidents.success && userIncidents.data) {
|
|
1907
|
-
const incidentData = Array.isArray(userIncidents.data) ? userIncidents.data : [userIncidents.data];
|
|
1908
|
-
const impactCounts = new Map();
|
|
1909
|
-
|
|
1910
|
-
incidentData.forEach(incident => {
|
|
1911
|
-
const impact = incident.impact || 'unknown';
|
|
1912
|
-
impactCounts.set(impact, (impactCounts.get(impact) || 0) + 1);
|
|
1913
|
-
});
|
|
1914
|
-
|
|
1915
|
-
return [{
|
|
1916
|
-
metric: 'user_impact_distribution',
|
|
1917
|
-
high_impact: impactCounts.get('1') || 0,
|
|
1918
|
-
medium_impact: impactCounts.get('2') || 0,
|
|
1919
|
-
low_impact: impactCounts.get('3') || 0,
|
|
1920
|
-
total_affected_users: incidentData.length,
|
|
1921
|
-
impact_trend: (impactCounts.get('1') || 0) > 5 ? 'increasing' : 'stable'
|
|
1922
|
-
}];
|
|
1923
|
-
}
|
|
1924
|
-
} catch (error) {
|
|
1925
|
-
return [{ error: 'Failed to analyze user impact trends', message: String(error) }];
|
|
1926
|
-
}
|
|
1927
|
-
return [{ message: 'No user impact data available' }];
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
private async predictIncidentVolume(timeframe: string): Promise<any[]> {
|
|
1931
|
-
// Get historical incident data
|
|
1932
|
-
const historicalData = await this.client.searchRecords('incident', 'sys_created_onONLast 30 days@javascript:gs.daysAgoStart(30)@javascript:gs.daysAgoEnd(0)', 1000);
|
|
1933
|
-
|
|
1934
|
-
const dailyVolume = new Map();
|
|
1935
|
-
if (historicalData.success) {
|
|
1936
|
-
historicalData.data.result.forEach((incident: any) => {
|
|
1937
|
-
const date = new Date(incident.sys_created_on).toDateString();
|
|
1938
|
-
dailyVolume.set(date, (dailyVolume.get(date) || 0) + 1);
|
|
1939
|
-
});
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
const avgDailyVolume = Array.from(dailyVolume.values()).reduce((a, b) => a + b, 0) / dailyVolume.size;
|
|
1943
|
-
|
|
1944
|
-
return [
|
|
1945
|
-
{
|
|
1946
|
-
metric: 'daily_incident_volume',
|
|
1947
|
-
predicted_value: Math.round(avgDailyVolume * 1.1), // Simple trend prediction
|
|
1948
|
-
current_average: Math.round(avgDailyVolume),
|
|
1949
|
-
trend: 'increasing',
|
|
1950
|
-
confidence: 0.6
|
|
1951
|
-
}
|
|
1952
|
-
];
|
|
1953
|
-
}
|
|
1954
|
-
|
|
1955
|
-
private async predictSystemFailure(timeframe: string): Promise<any[]> {
|
|
1956
|
-
// Analyze problem records and incident patterns
|
|
1957
|
-
const problems = await this.client.searchRecords('problem', 'sys_created_onONLast 30 days@javascript:gs.daysAgoStart(30)@javascript:gs.daysAgoEnd(0)', 100);
|
|
1958
|
-
|
|
1959
|
-
const systemFailures = problems.success ? problems.data.result.filter((problem: any) =>
|
|
1960
|
-
(problem.short_description || '').toLowerCase().includes('system') ||
|
|
1961
|
-
(problem.short_description || '').toLowerCase().includes('failure') ||
|
|
1962
|
-
(problem.short_description || '').toLowerCase().includes('outage')
|
|
1963
|
-
) : [];
|
|
1964
|
-
|
|
1965
|
-
return [
|
|
1966
|
-
{
|
|
1967
|
-
metric: 'system_failure_risk',
|
|
1968
|
-
predicted_value: systemFailures.length > 5 ? 'high' : 'medium',
|
|
1969
|
-
current_problems: systemFailures.length,
|
|
1970
|
-
risk_factors: ['Historical system problems', 'Incident patterns'],
|
|
1971
|
-
confidence: 0.5
|
|
1972
|
-
}
|
|
1973
|
-
];
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
private async predictResourceExhaustion(timeframe: string): Promise<any[]> {
|
|
1977
|
-
// This would require integration with monitoring systems
|
|
1978
|
-
return [
|
|
1979
|
-
{
|
|
1980
|
-
metric: 'resource_exhaustion_risk',
|
|
1981
|
-
predicted_value: 'low',
|
|
1982
|
-
message: 'Resource exhaustion prediction requires monitoring system integration',
|
|
1983
|
-
confidence: 0.3
|
|
1984
|
-
}
|
|
1985
|
-
];
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
private async predictUserImpact(timeframe: string): Promise<any[]> {
|
|
1989
|
-
// Analyze user-reported incidents
|
|
1990
|
-
const userIncidents = await this.client.searchRecords('incident', 'sys_created_onONLast 7 days@javascript:gs.daysAgoStart(7)@javascript:gs.daysAgoEnd(0)^priority=1', 100);
|
|
1991
|
-
|
|
1992
|
-
const impactLevels = new Map();
|
|
1993
|
-
if (userIncidents.success) {
|
|
1994
|
-
userIncidents.data.result.forEach((incident: any) => {
|
|
1995
|
-
const impact = incident.impact || 'unknown';
|
|
1996
|
-
impactLevels.set(impact, (impactLevels.get(impact) || 0) + 1);
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
return [
|
|
2001
|
-
{
|
|
2002
|
-
metric: 'user_impact_prediction',
|
|
2003
|
-
predicted_value: impactLevels.get('1') > 5 ? 'high' : 'medium',
|
|
2004
|
-
current_high_impact: impactLevels.get('1') || 0,
|
|
2005
|
-
trend_factors: ['High priority incidents', 'User complaints'],
|
|
2006
|
-
confidence: 0.6
|
|
2007
|
-
}
|
|
2008
|
-
];
|
|
2009
|
-
}
|
|
2010
|
-
|
|
2011
|
-
private async handleCatalogItemManager(args: any) {
|
|
2012
|
-
const { action, item_id, ...params } = args;
|
|
2013
|
-
|
|
2014
|
-
logger.info(`Catalog item manager action: ${action}`, { item_id, params });
|
|
2015
|
-
|
|
2016
|
-
try {
|
|
2017
|
-
switch (action) {
|
|
2018
|
-
case 'create': {
|
|
2019
|
-
// Get default catalog if none provided
|
|
2020
|
-
let catalogId = params.sc_catalogs || params.catalog_id;
|
|
2021
|
-
let categoryId = params.sc_categories || params.category_id;
|
|
2022
|
-
|
|
2023
|
-
if (!catalogId) {
|
|
2024
|
-
// Find default Service Catalog
|
|
2025
|
-
const defaultCatalogResult = await this.client.searchRecords(
|
|
2026
|
-
'sc_catalog',
|
|
2027
|
-
'active=true^ORDERBYsys_created_on',
|
|
2028
|
-
1
|
|
2029
|
-
);
|
|
2030
|
-
|
|
2031
|
-
if (defaultCatalogResult.success && defaultCatalogResult.data?.result?.length > 0) {
|
|
2032
|
-
catalogId = defaultCatalogResult.data.result[0].sys_id;
|
|
2033
|
-
logger.info('Using default catalog:', {
|
|
2034
|
-
catalogId,
|
|
2035
|
-
catalogName: defaultCatalogResult.data.result[0].title
|
|
2036
|
-
});
|
|
2037
|
-
} else {
|
|
2038
|
-
logger.warn('No catalogs found, catalog item may not be visible');
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
|
|
2042
|
-
if (!categoryId) {
|
|
2043
|
-
// Find a default category like "Hardware" or "General"
|
|
2044
|
-
const defaultCategoryResult = await this.client.searchRecords(
|
|
2045
|
-
'sc_category',
|
|
2046
|
-
'active=true^titleLIKEHardware^ORtitleLIKEGeneral^ORtitleLIKEIT^ORDERBYsys_created_on',
|
|
2047
|
-
1
|
|
2048
|
-
);
|
|
2049
|
-
|
|
2050
|
-
if (defaultCategoryResult.success && defaultCategoryResult.data?.result?.length > 0) {
|
|
2051
|
-
categoryId = defaultCategoryResult.data.result[0].sys_id;
|
|
2052
|
-
logger.info('Using default category:', {
|
|
2053
|
-
categoryId,
|
|
2054
|
-
categoryName: defaultCategoryResult.data.result[0].title
|
|
2055
|
-
});
|
|
2056
|
-
}
|
|
2057
|
-
}
|
|
2058
|
-
|
|
2059
|
-
const catalogItem = {
|
|
2060
|
-
name: params.name,
|
|
2061
|
-
short_description: params.short_description,
|
|
2062
|
-
description: params.description,
|
|
2063
|
-
price: params.price || '0',
|
|
2064
|
-
recurring_price: params.recurring_price || '0',
|
|
2065
|
-
billable: params.billable || false,
|
|
2066
|
-
active: params.active !== false,
|
|
2067
|
-
workflow: params.workflow,
|
|
2068
|
-
picture: params.picture,
|
|
2069
|
-
sc_catalogs: catalogId,
|
|
2070
|
-
sc_categories: categoryId,
|
|
2071
|
-
sys_class_name: 'sc_cat_item'
|
|
2072
|
-
};
|
|
2073
|
-
|
|
2074
|
-
const result = await this.client.createRecord('sc_cat_item', catalogItem);
|
|
2075
|
-
|
|
2076
|
-
if (result.success && params.variables && params.variables.length > 0) {
|
|
2077
|
-
// Add variables to the catalog item
|
|
2078
|
-
for (const variable of params.variables) {
|
|
2079
|
-
await this.createCatalogVariable(result.data.sys_id, variable);
|
|
2080
|
-
}
|
|
2081
|
-
}
|
|
2082
|
-
|
|
2083
|
-
return {
|
|
2084
|
-
content: [{
|
|
2085
|
-
type: 'text',
|
|
2086
|
-
text: result.success
|
|
2087
|
-
? `✅ Catalog item created successfully!\n\nSys ID: ${result.data.sys_id}\nName: ${result.data.name}\n\nView at: ${this.getServiceNowUrl()}/nav_to.do?uri=sc_cat_item.do?sys_id=${result.data.sys_id}`
|
|
2088
|
-
: `❌ Failed to create catalog item: ${result.error}`
|
|
2089
|
-
}]
|
|
2090
|
-
};
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
case 'update': {
|
|
2094
|
-
if (!item_id) {
|
|
2095
|
-
throw new Error('item_id is required for update action');
|
|
2096
|
-
}
|
|
2097
|
-
|
|
2098
|
-
const updateData: any = {};
|
|
2099
|
-
if (params.name) updateData.name = params.name;
|
|
2100
|
-
if (params.short_description) updateData.short_description = params.short_description;
|
|
2101
|
-
if (params.description) updateData.description = params.description;
|
|
2102
|
-
if (params.price) updateData.price = params.price;
|
|
2103
|
-
if (params.recurring_price) updateData.recurring_price = params.recurring_price;
|
|
2104
|
-
if (params.billable !== undefined) updateData.billable = params.billable;
|
|
2105
|
-
if (params.active !== undefined) updateData.active = params.active;
|
|
2106
|
-
if (params.workflow) updateData.workflow = params.workflow;
|
|
2107
|
-
if (params.sc_catalogs) updateData.sc_catalogs = params.sc_catalogs;
|
|
2108
|
-
if (params.sc_categories) updateData.sc_categories = params.sc_categories;
|
|
2109
|
-
|
|
2110
|
-
const result = await this.client.updateRecord('sc_cat_item', item_id, updateData);
|
|
2111
|
-
|
|
2112
|
-
return {
|
|
2113
|
-
content: [{
|
|
2114
|
-
type: 'text',
|
|
2115
|
-
text: result.success
|
|
2116
|
-
? `✅ Catalog item updated successfully!\n\nSys ID: ${item_id}\n\nView at: ${this.getServiceNowUrl()}/nav_to.do?uri=sc_cat_item.do?sys_id=${item_id}`
|
|
2117
|
-
: `❌ Failed to update catalog item: ${result.error}`
|
|
2118
|
-
}]
|
|
2119
|
-
};
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
case 'list': {
|
|
2123
|
-
const query = params.category_id ? `sc_categories=${params.category_id}` : 'active=true';
|
|
2124
|
-
const result = await this.client.searchRecords('sc_cat_item', query, 50);
|
|
2125
|
-
|
|
2126
|
-
if (result.success) {
|
|
2127
|
-
const items = result.data.result.map((item: any) => ({
|
|
2128
|
-
sys_id: item.sys_id,
|
|
2129
|
-
name: item.name,
|
|
2130
|
-
short_description: item.short_description,
|
|
2131
|
-
price: item.price,
|
|
2132
|
-
active: item.active,
|
|
2133
|
-
category: item.category?.display_value
|
|
2134
|
-
}));
|
|
2135
|
-
|
|
2136
|
-
return {
|
|
2137
|
-
content: [{
|
|
2138
|
-
type: 'text',
|
|
2139
|
-
text: `📋 Found ${items.length} catalog items:\n\n${JSON.stringify(items, null, 2)}`
|
|
2140
|
-
}]
|
|
2141
|
-
};
|
|
2142
|
-
} else {
|
|
2143
|
-
throw new Error(`Failed to list catalog items: ${result.error}`);
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
|
|
2147
|
-
case 'get': {
|
|
2148
|
-
if (!item_id) {
|
|
2149
|
-
throw new Error('item_id is required for get action');
|
|
2150
|
-
}
|
|
2151
|
-
|
|
2152
|
-
const result = await this.client.getRecord('sc_cat_item', item_id);
|
|
2153
|
-
|
|
2154
|
-
if (result.success) {
|
|
2155
|
-
// Get variables for the item
|
|
2156
|
-
const variables = await this.client.searchRecords('item_option_new', `cat_item=${item_id}`, 50);
|
|
2157
|
-
|
|
2158
|
-
return {
|
|
2159
|
-
content: [{
|
|
2160
|
-
type: 'text',
|
|
2161
|
-
text: `📋 Catalog Item Details:\n\n${JSON.stringify(result.data, null, 2)}\n\n📊 Variables:\n${JSON.stringify(variables.data?.result || [], null, 2)}`
|
|
2162
|
-
}]
|
|
2163
|
-
};
|
|
2164
|
-
} else {
|
|
2165
|
-
throw new Error(`Failed to get catalog item: ${result.error}`);
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
case 'add_variable': {
|
|
2170
|
-
if (!item_id) {
|
|
2171
|
-
throw new Error('item_id is required for add_variable action');
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
if (!params.variables || params.variables.length === 0) {
|
|
2175
|
-
throw new Error('variables array is required');
|
|
2176
|
-
}
|
|
2177
|
-
|
|
2178
|
-
const results = [];
|
|
2179
|
-
for (const variable of params.variables) {
|
|
2180
|
-
const result = await this.createCatalogVariable(item_id, variable);
|
|
2181
|
-
results.push(result);
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
return {
|
|
2185
|
-
content: [{
|
|
2186
|
-
type: 'text',
|
|
2187
|
-
text: `✅ Added ${results.length} variables to catalog item ${item_id}`
|
|
2188
|
-
}]
|
|
2189
|
-
};
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
case 'set_workflow': {
|
|
2193
|
-
if (!item_id) {
|
|
2194
|
-
throw new Error('item_id is required for set_workflow action');
|
|
2195
|
-
}
|
|
2196
|
-
|
|
2197
|
-
if (!params.workflow) {
|
|
2198
|
-
throw new Error('workflow is required');
|
|
2199
|
-
}
|
|
2200
|
-
|
|
2201
|
-
const result = await this.client.updateRecord('sc_cat_item', item_id, {
|
|
2202
|
-
workflow: params.workflow
|
|
2203
|
-
});
|
|
2204
|
-
|
|
2205
|
-
return {
|
|
2206
|
-
content: [{
|
|
2207
|
-
type: 'text',
|
|
2208
|
-
text: result.success
|
|
2209
|
-
? `✅ Workflow set successfully for catalog item ${item_id}`
|
|
2210
|
-
: `❌ Failed to set workflow: ${result.error}`
|
|
2211
|
-
}]
|
|
2212
|
-
};
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
case 'publish': {
|
|
2216
|
-
if (!item_id) {
|
|
2217
|
-
throw new Error('item_id is required for publish action');
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
const result = await this.client.updateRecord('sc_cat_item', item_id, {
|
|
2221
|
-
active: true
|
|
2222
|
-
});
|
|
2223
|
-
|
|
2224
|
-
return {
|
|
2225
|
-
content: [{
|
|
2226
|
-
type: 'text',
|
|
2227
|
-
text: result.success
|
|
2228
|
-
? `✅ Catalog item ${item_id} published successfully`
|
|
2229
|
-
: `❌ Failed to publish catalog item: ${result.error}`
|
|
2230
|
-
}]
|
|
2231
|
-
};
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
case 'retire': {
|
|
2235
|
-
if (!item_id) {
|
|
2236
|
-
throw new Error('item_id is required for retire action');
|
|
2237
|
-
}
|
|
2238
|
-
|
|
2239
|
-
const result = await this.client.updateRecord('sc_cat_item', item_id, {
|
|
2240
|
-
active: false
|
|
2241
|
-
});
|
|
2242
|
-
|
|
2243
|
-
return {
|
|
2244
|
-
content: [{
|
|
2245
|
-
type: 'text',
|
|
2246
|
-
text: result.success
|
|
2247
|
-
? `✅ Catalog item ${item_id} retired successfully`
|
|
2248
|
-
: `❌ Failed to retire catalog item: ${result.error}`
|
|
2249
|
-
}]
|
|
2250
|
-
};
|
|
2251
|
-
}
|
|
2252
|
-
|
|
2253
|
-
default:
|
|
2254
|
-
throw new Error(`Unknown action: ${action}`);
|
|
2255
|
-
}
|
|
2256
|
-
} catch (error) {
|
|
2257
|
-
logger.error('Error in catalog item manager:', error);
|
|
2258
|
-
throw new McpError(ErrorCode.InternalError, `Catalog item manager error: ${error}`);
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
|
|
2262
|
-
private async createCatalogVariable(catalogItemId: string, variable: any) {
|
|
2263
|
-
const variableData: any = {
|
|
2264
|
-
cat_item: catalogItemId,
|
|
2265
|
-
name: variable.name,
|
|
2266
|
-
question_text: variable.label || variable.name,
|
|
2267
|
-
type: this.mapVariableType(variable.type),
|
|
2268
|
-
mandatory: variable.mandatory || false,
|
|
2269
|
-
default_value: variable.default_value || '',
|
|
2270
|
-
order: variable.order || 100
|
|
2271
|
-
};
|
|
2272
|
-
|
|
2273
|
-
// Handle select box choices
|
|
2274
|
-
if (variable.type === 'select_box' && variable.choices) {
|
|
2275
|
-
variableData.lookup_table = 'question_choice';
|
|
2276
|
-
// Note: In a real implementation, you'd need to create choice records
|
|
2277
|
-
}
|
|
2278
|
-
|
|
2279
|
-
return await this.client.createRecord('item_option_new', variableData);
|
|
2280
|
-
}
|
|
2281
|
-
|
|
2282
|
-
private mapVariableType(type: string): string {
|
|
2283
|
-
const typeMap: Record<string, string> = {
|
|
2284
|
-
'single_line_text': '6', // Single Line Text
|
|
2285
|
-
'multi_line_text': '2', // Multi Line Text
|
|
2286
|
-
'select_box': '3', // Select Box
|
|
2287
|
-
'checkbox': '7', // Checkbox
|
|
2288
|
-
'reference': '8', // Reference
|
|
2289
|
-
'date': '9', // Date
|
|
2290
|
-
'datetime': '10' // Date/Time
|
|
2291
|
-
};
|
|
2292
|
-
|
|
2293
|
-
return typeMap[type] || '6'; // Default to single line text
|
|
2294
|
-
}
|
|
2295
|
-
|
|
2296
|
-
private async handleCatalogItemSearch(args: any) {
|
|
2297
|
-
const { query, include_inactive = false, fuzzy_match = true, category_filter, limit = 50, include_variables = false } = args;
|
|
2298
|
-
|
|
2299
|
-
logger.info(`Searching catalog items for: ${query}`, { fuzzy_match, category_filter, limit });
|
|
2300
|
-
|
|
2301
|
-
try {
|
|
2302
|
-
// Build search queries
|
|
2303
|
-
const searchQueries = [];
|
|
2304
|
-
|
|
2305
|
-
// Exact match
|
|
2306
|
-
searchQueries.push(`name=${query}`);
|
|
2307
|
-
|
|
2308
|
-
// Contains match
|
|
2309
|
-
searchQueries.push(`nameLIKE${query}`);
|
|
2310
|
-
searchQueries.push(`short_descriptionLIKE${query}`);
|
|
2311
|
-
|
|
2312
|
-
// Fuzzy matching for similar items
|
|
2313
|
-
if (fuzzy_match) {
|
|
2314
|
-
// Handle common variations
|
|
2315
|
-
const variations = this.generateSearchVariations(query);
|
|
2316
|
-
variations.forEach(variation => {
|
|
2317
|
-
searchQueries.push(`nameLIKE${variation}`);
|
|
2318
|
-
searchQueries.push(`short_descriptionLIKE${variation}`);
|
|
2319
|
-
});
|
|
2320
|
-
}
|
|
2321
|
-
|
|
2322
|
-
// Add active filter
|
|
2323
|
-
const activeFilter = include_inactive ? '' : '^active=true';
|
|
2324
|
-
|
|
2325
|
-
// Add category filter if provided
|
|
2326
|
-
let categoryFilter = '';
|
|
2327
|
-
if (category_filter) {
|
|
2328
|
-
// First try to find the category
|
|
2329
|
-
const categoryResult = await this.client.searchRecords('sc_category',
|
|
2330
|
-
`nameLIKE${category_filter}^ORtitle=${category_filter}^ORsys_id=${category_filter}`,
|
|
2331
|
-
10);
|
|
2332
|
-
if (categoryResult.success && categoryResult.data?.result?.length > 0) {
|
|
2333
|
-
// Try to find the most relevant category
|
|
2334
|
-
const exactMatch = categoryResult.data.result.find((c: any) =>
|
|
2335
|
-
c.name.toLowerCase() === category_filter.toLowerCase());
|
|
2336
|
-
const category = exactMatch || categoryResult.data.result[0];
|
|
2337
|
-
categoryFilter = `^sc_categoriesLIKE${category.sys_id}`;
|
|
2338
|
-
}
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2341
|
-
// Execute searches
|
|
2342
|
-
const allResults = new Map();
|
|
2343
|
-
|
|
2344
|
-
for (const searchQuery of searchQueries) {
|
|
2345
|
-
const fullQuery = searchQuery + activeFilter + categoryFilter;
|
|
2346
|
-
const result = await this.client.searchRecords('sc_cat_item', fullQuery, limit);
|
|
2347
|
-
|
|
2348
|
-
if (result.success && result.data?.result) {
|
|
2349
|
-
result.data.result.forEach((item: any) => {
|
|
2350
|
-
if (!allResults.has(item.sys_id)) {
|
|
2351
|
-
// Filter out items that don't make sense for the search
|
|
2352
|
-
const itemName = (item.name || '').toLowerCase();
|
|
2353
|
-
const itemDesc = (item.short_description || '').toLowerCase();
|
|
2354
|
-
|
|
2355
|
-
// Skip items that are clearly not what the user is looking for
|
|
2356
|
-
if (this.shouldExcludeItem(query, itemName, itemDesc)) {
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
|
|
2360
|
-
allResults.set(item.sys_id, {
|
|
2361
|
-
sys_id: item.sys_id,
|
|
2362
|
-
name: item.name,
|
|
2363
|
-
short_description: item.short_description,
|
|
2364
|
-
price: item.price,
|
|
2365
|
-
recurring_price: item.recurring_price,
|
|
2366
|
-
active: item.active,
|
|
2367
|
-
category: item.sc_categories?.display_value,
|
|
2368
|
-
catalog: item.sc_catalogs?.display_value,
|
|
2369
|
-
model_number: item.model,
|
|
2370
|
-
manufacturer: item.vendor,
|
|
2371
|
-
variables: []
|
|
2372
|
-
});
|
|
2373
|
-
}
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
// Get variables if requested
|
|
2379
|
-
if (include_variables && allResults.size > 0) {
|
|
2380
|
-
for (const [sys_id, item] of allResults) {
|
|
2381
|
-
const variablesResult = await this.client.searchRecords('item_option_new', `cat_item=${sys_id}`, 50);
|
|
2382
|
-
if (variablesResult.success && variablesResult.data?.result) {
|
|
2383
|
-
item.variables = variablesResult.data.result.map((v: any) => ({
|
|
2384
|
-
name: v.name,
|
|
2385
|
-
label: v.question_text,
|
|
2386
|
-
type: v.type,
|
|
2387
|
-
mandatory: v.mandatory,
|
|
2388
|
-
default_value: v.default_value
|
|
2389
|
-
}));
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
const resultsArray = Array.from(allResults.values());
|
|
2395
|
-
|
|
2396
|
-
// Sort by relevance (exact matches first)
|
|
2397
|
-
resultsArray.sort((a, b) => {
|
|
2398
|
-
const aExact = a.name.toLowerCase() === query.toLowerCase();
|
|
2399
|
-
const bExact = b.name.toLowerCase() === query.toLowerCase();
|
|
2400
|
-
if (aExact && !bExact) return -1;
|
|
2401
|
-
if (!aExact && bExact) return 1;
|
|
2402
|
-
return 0;
|
|
2403
|
-
});
|
|
2404
|
-
|
|
2405
|
-
if (resultsArray.length === 0) {
|
|
2406
|
-
// Provide suggestions if no results found
|
|
2407
|
-
const suggestions = this.generateSearchSuggestions(query);
|
|
2408
|
-
return {
|
|
2409
|
-
content: [{
|
|
2410
|
-
type: 'text',
|
|
2411
|
-
text: `❌ No catalog items found for "${query}"\n\n` +
|
|
2412
|
-
`💡 Suggestions:\n` +
|
|
2413
|
-
suggestions.map(s => `- Try searching for "${s}"`).join('\n') +
|
|
2414
|
-
`\n\n💡 Tips:\n` +
|
|
2415
|
-
`- Check if the item name is spelled correctly\n` +
|
|
2416
|
-
`- Try searching with partial names (e.g., "iPhone" instead of "iPhone 6S")\n` +
|
|
2417
|
-
`- Use category filter to narrow down results\n` +
|
|
2418
|
-
`- Enable include_inactive if looking for retired items`
|
|
2419
|
-
}]
|
|
2420
|
-
};
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
|
-
// Format results
|
|
2424
|
-
let resultText = `🔍 Found ${resultsArray.length} catalog items matching "${query}":\n\n`;
|
|
2425
|
-
|
|
2426
|
-
resultsArray.forEach((item, index) => {
|
|
2427
|
-
resultText += `${index + 1}. **${item.name}** (${item.sys_id})\n`;
|
|
2428
|
-
resultText += ` 📝 ${item.short_description || 'No description'}\n`;
|
|
2429
|
-
resultText += ` 💰 Price: $${item.price || '0'} ${item.recurring_price ? `(+ $${item.recurring_price}/month)` : ''}\n`;
|
|
2430
|
-
resultText += ` 📁 Category: ${item.category || 'Uncategorized'}\n`;
|
|
2431
|
-
resultText += ` 📚 Catalog: ${item.catalog || 'Default'}\n`;
|
|
2432
|
-
if (item.model_number) {
|
|
2433
|
-
resultText += ` 🔢 Model: ${item.model_number}\n`;
|
|
2434
|
-
}
|
|
2435
|
-
if (item.manufacturer) {
|
|
2436
|
-
resultText += ` 🏭 Manufacturer: ${item.manufacturer}\n`;
|
|
2437
|
-
}
|
|
2438
|
-
resultText += ` ✅ Status: ${item.active ? 'Active' : 'Inactive'}\n`;
|
|
2439
|
-
|
|
2440
|
-
if (item.variables && item.variables.length > 0) {
|
|
2441
|
-
resultText += ` 📋 Variables (${item.variables.length}):\n`;
|
|
2442
|
-
item.variables.forEach((v: any) => {
|
|
2443
|
-
resultText += ` - ${v.label} (${v.name}) - ${v.type}${v.mandatory ? ' *Required' : ''}\n`;
|
|
2444
|
-
});
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
resultText += '\n';
|
|
2448
|
-
});
|
|
2449
|
-
|
|
2450
|
-
resultText += `\n💡 Use snow_catalog_item_manager to create or update catalog items`;
|
|
2451
|
-
|
|
2452
|
-
return {
|
|
2453
|
-
content: [{
|
|
2454
|
-
type: 'text',
|
|
2455
|
-
text: resultText
|
|
2456
|
-
}]
|
|
2457
|
-
};
|
|
2458
|
-
} catch (error) {
|
|
2459
|
-
logger.error('Error searching catalog items:', error);
|
|
2460
|
-
throw new McpError(ErrorCode.InternalError, `Catalog search error: ${error}`);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
|
|
2464
|
-
private generateSearchVariations(query: string): string[] {
|
|
2465
|
-
const variations = [];
|
|
2466
|
-
const lowerQuery = query.toLowerCase();
|
|
2467
|
-
|
|
2468
|
-
// Handle common product variations
|
|
2469
|
-
if (lowerQuery.includes('iphone')) {
|
|
2470
|
-
// Add variations without spaces, with different numbers
|
|
2471
|
-
const baseModel = query.replace(/iphone\s*/i, 'iPhone ');
|
|
2472
|
-
variations.push(baseModel);
|
|
2473
|
-
variations.push(baseModel.replace(' ', ''));
|
|
2474
|
-
|
|
2475
|
-
// Try different model numbers
|
|
2476
|
-
const modelMatch = query.match(/\d+/);
|
|
2477
|
-
if (modelMatch) {
|
|
2478
|
-
const modelNum = parseInt(modelMatch[0]);
|
|
2479
|
-
variations.push(query.replace(modelNum.toString(), (modelNum + 1).toString()));
|
|
2480
|
-
variations.push(query.replace(modelNum.toString(), (modelNum - 1).toString()));
|
|
2481
|
-
}
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
// Handle laptop/computer variations
|
|
2485
|
-
if (lowerQuery.includes('laptop') || lowerQuery.includes('computer')) {
|
|
2486
|
-
variations.push('notebook', 'macbook', 'thinkpad', 'dell', 'hp');
|
|
2487
|
-
}
|
|
2488
|
-
|
|
2489
|
-
// Handle phone variations
|
|
2490
|
-
if (lowerQuery.includes('phone') || lowerQuery.includes('mobile')) {
|
|
2491
|
-
variations.push('smartphone', 'android', 'samsung', 'pixel');
|
|
2492
|
-
}
|
|
2493
|
-
|
|
2494
|
-
// Handle monitor/display variations
|
|
2495
|
-
if (lowerQuery.includes('monitor') || lowerQuery.includes('display') ||
|
|
2496
|
-
lowerQuery.includes('screen') || lowerQuery.includes('lcd')) {
|
|
2497
|
-
variations.push('monitor', 'display', 'screen', 'lcd', 'led', 'desktop monitor',
|
|
2498
|
-
'computer monitor', 'external display', 'lcd monitor', 'led monitor',
|
|
2499
|
-
'dell monitor', 'hp monitor', 'samsung monitor', 'lg monitor');
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
// Handle desktop/workstation variations
|
|
2503
|
-
if (lowerQuery.includes('desktop') || lowerQuery.includes('workstation')) {
|
|
2504
|
-
variations.push('desktop computer', 'workstation', 'pc', 'desktop pc',
|
|
2505
|
-
'dell desktop', 'hp desktop', 'lenovo desktop');
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
// Handle specific hardware terms
|
|
2509
|
-
if (lowerQuery.includes('hardware') || lowerQuery.includes('equipment')) {
|
|
2510
|
-
// Extract specific items from the query
|
|
2511
|
-
const specificItems = [];
|
|
2512
|
-
if (lowerQuery.includes('monitor') || lowerQuery.includes('display') || lowerQuery.includes('screen')) {
|
|
2513
|
-
specificItems.push('monitor', 'display');
|
|
2514
|
-
}
|
|
2515
|
-
if (lowerQuery.includes('desktop')) {
|
|
2516
|
-
specificItems.push('desktop', 'computer');
|
|
2517
|
-
}
|
|
2518
|
-
if (lowerQuery.includes('laptop')) {
|
|
2519
|
-
specificItems.push('laptop', 'notebook');
|
|
2520
|
-
}
|
|
2521
|
-
|
|
2522
|
-
// If we found specific items, use those instead of generic "hardware"
|
|
2523
|
-
if (specificItems.length > 0) {
|
|
2524
|
-
variations.push(...specificItems);
|
|
2525
|
-
}
|
|
2526
|
-
}
|
|
2527
|
-
|
|
2528
|
-
// Remove duplicates and empty strings
|
|
2529
|
-
return [...new Set(variations)].filter(v => v && v.length > 0);
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
private generateSearchSuggestions(query: string): string[] {
|
|
2533
|
-
const suggestions = [];
|
|
2534
|
-
const lowerQuery = query.toLowerCase();
|
|
2535
|
-
|
|
2536
|
-
if (lowerQuery.includes('iphone')) {
|
|
2537
|
-
suggestions.push('iPhone', 'Apple iPhone', 'iOS device', 'Apple mobile');
|
|
2538
|
-
} else if (lowerQuery.includes('laptop')) {
|
|
2539
|
-
suggestions.push('notebook', 'computer', 'workstation', 'MacBook', 'ThinkPad');
|
|
2540
|
-
} else if (lowerQuery.includes('software')) {
|
|
2541
|
-
suggestions.push('license', 'application', 'subscription', 'SaaS');
|
|
2542
|
-
} else if (lowerQuery.includes('monitor') || lowerQuery.includes('display') ||
|
|
2543
|
-
lowerQuery.includes('screen') || lowerQuery.includes('lcd')) {
|
|
2544
|
-
suggestions.push('monitor', 'display', 'external monitor', 'desktop monitor',
|
|
2545
|
-
'Dell monitor', 'HP monitor', 'Samsung monitor');
|
|
2546
|
-
} else if (lowerQuery.includes('desktop')) {
|
|
2547
|
-
suggestions.push('desktop computer', 'workstation', 'PC', 'desktop PC',
|
|
2548
|
-
'Dell desktop', 'HP desktop');
|
|
2549
|
-
} else if (lowerQuery.includes('hardware')) {
|
|
2550
|
-
// For hardware searches, suggest specific item types
|
|
2551
|
-
suggestions.push('monitor', 'keyboard', 'mouse', 'laptop', 'desktop',
|
|
2552
|
-
'printer', 'scanner', 'docking station', 'headset');
|
|
2553
|
-
} else {
|
|
2554
|
-
// Generic suggestions based on the query
|
|
2555
|
-
const words = query.split(' ');
|
|
2556
|
-
if (words.length > 1) {
|
|
2557
|
-
// Try individual words
|
|
2558
|
-
suggestions.push(...words.filter(w => w.length > 3));
|
|
2559
|
-
// Try first and last word
|
|
2560
|
-
suggestions.push(words[0], words[words.length - 1]);
|
|
2561
|
-
}
|
|
2562
|
-
|
|
2563
|
-
// Add generic category suggestions
|
|
2564
|
-
suggestions.push('hardware', 'software', 'equipment', 'device', 'accessory');
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
return [...new Set(suggestions)].filter(s => s && s !== query && s.length > 0);
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
private shouldExcludeItem(query: string, itemName: string, itemDesc: string): boolean {
|
|
2571
|
-
const lowerQuery = query.toLowerCase();
|
|
2572
|
-
|
|
2573
|
-
// If searching for hardware/monitors/displays, exclude service/process items
|
|
2574
|
-
if ((lowerQuery.includes('hardware') || lowerQuery.includes('monitor') ||
|
|
2575
|
-
lowerQuery.includes('display') || lowerQuery.includes('screen') ||
|
|
2576
|
-
lowerQuery.includes('desktop') || lowerQuery.includes('equipment')) &&
|
|
2577
|
-
(itemName.includes('decommission') || itemName.includes('service') ||
|
|
2578
|
-
itemName.includes('process') || itemName.includes('request') ||
|
|
2579
|
-
itemName.includes('removal') || itemName.includes('decomm') ||
|
|
2580
|
-
itemDesc.includes('decommission') || itemDesc.includes('service process'))) {
|
|
2581
|
-
return true;
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
// If searching for specific hardware, exclude unrelated items
|
|
2585
|
-
if (lowerQuery.includes('monitor') || lowerQuery.includes('display') || lowerQuery.includes('screen')) {
|
|
2586
|
-
// Exclude items that are clearly not monitors
|
|
2587
|
-
if ((itemName.includes('server') || itemName.includes('controller') ||
|
|
2588
|
-
itemName.includes('software') || itemName.includes('license') ||
|
|
2589
|
-
itemName.includes('training') || itemName.includes('support')) &&
|
|
2590
|
-
!itemName.includes('monitor') && !itemName.includes('display') &&
|
|
2591
|
-
!itemName.includes('screen')) {
|
|
2592
|
-
return true;
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
|
|
2596
|
-
// If searching for desktops, exclude non-desktop items
|
|
2597
|
-
if (lowerQuery.includes('desktop')) {
|
|
2598
|
-
if ((itemName.includes('mobile') || itemName.includes('phone') ||
|
|
2599
|
-
itemName.includes('tablet') || itemName.includes('service')) &&
|
|
2600
|
-
!itemName.includes('desktop')) {
|
|
2601
|
-
return true;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
|
|
2605
|
-
return false;
|
|
2606
|
-
}
|
|
2607
|
-
|
|
2608
|
-
private async handleCleanupTestArtifacts(args: any) {
|
|
2609
|
-
const {
|
|
2610
|
-
artifact_types = ['catalog_items', 'flows', 'users'],
|
|
2611
|
-
test_patterns = ['Test%', 'Mock%', 'Demo%', '%_test_%', '%test', '%mock'],
|
|
2612
|
-
max_age_hours = 1,
|
|
2613
|
-
dry_run = false,
|
|
2614
|
-
preserve_update_set_entries = true,
|
|
2615
|
-
update_set_filter
|
|
2616
|
-
} = args;
|
|
2617
|
-
|
|
2618
|
-
logger.info('Starting test artifact cleanup', {
|
|
2619
|
-
artifact_types,
|
|
2620
|
-
test_patterns,
|
|
2621
|
-
max_age_hours,
|
|
2622
|
-
dry_run
|
|
2623
|
-
});
|
|
2624
|
-
|
|
2625
|
-
const cleanupResults = {
|
|
2626
|
-
start_time: new Date().toISOString(),
|
|
2627
|
-
dry_run,
|
|
2628
|
-
artifacts_found: {
|
|
2629
|
-
catalog_items: [],
|
|
2630
|
-
flows: [],
|
|
2631
|
-
users: [],
|
|
2632
|
-
requests: []
|
|
2633
|
-
} as any,
|
|
2634
|
-
artifacts_deleted: {
|
|
2635
|
-
catalog_items: 0,
|
|
2636
|
-
flows: 0,
|
|
2637
|
-
users: 0,
|
|
2638
|
-
requests: 0
|
|
2639
|
-
},
|
|
2640
|
-
update_set_entries_preserved: 0,
|
|
2641
|
-
errors: [] as string[],
|
|
2642
|
-
summary: ''
|
|
2643
|
-
};
|
|
2644
|
-
|
|
2645
|
-
try {
|
|
2646
|
-
// Calculate cutoff time
|
|
2647
|
-
const cutoffTime = new Date();
|
|
2648
|
-
cutoffTime.setHours(cutoffTime.getHours() - max_age_hours);
|
|
2649
|
-
const cutoffISO = cutoffTime.toISOString();
|
|
2650
|
-
|
|
2651
|
-
// Process each artifact type
|
|
2652
|
-
for (const artifactType of artifact_types) {
|
|
2653
|
-
if (artifactType === 'all' || artifact_types.includes('catalog_items')) {
|
|
2654
|
-
await this.cleanupTestCatalogItems(
|
|
2655
|
-
test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter
|
|
2656
|
-
);
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
// Flow cleanup removed in v1.4.38 - flows no longer supported
|
|
2660
|
-
|
|
2661
|
-
if (artifactType === 'all' || artifact_types.includes('users')) {
|
|
2662
|
-
await this.cleanupTestUsers(
|
|
2663
|
-
test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter
|
|
2664
|
-
);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
if (artifactType === 'all' || artifact_types.includes('requests')) {
|
|
2668
|
-
await this.cleanupTestRequests(
|
|
2669
|
-
test_patterns, cutoffISO, dry_run, cleanupResults, update_set_filter
|
|
2670
|
-
);
|
|
2671
|
-
}
|
|
2672
|
-
}
|
|
2673
|
-
|
|
2674
|
-
// Generate summary
|
|
2675
|
-
const totalFound = Object.values(cleanupResults.artifacts_found)
|
|
2676
|
-
.reduce((sum, items) => (typeof sum === 'number' ? sum : 0) + (Array.isArray(items) ? items.length : 0), 0);
|
|
2677
|
-
const totalDeleted = Object.values(cleanupResults.artifacts_deleted)
|
|
2678
|
-
.reduce((sum, count) => sum + (typeof count === 'number' ? count : 0), 0);
|
|
2679
|
-
|
|
2680
|
-
cleanupResults.summary = dry_run
|
|
2681
|
-
? `🔍 Dry Run: Found ${totalFound} test artifacts that would be cleaned up`
|
|
2682
|
-
: `🧹 Cleaned up ${totalDeleted} test artifacts successfully`;
|
|
2683
|
-
|
|
2684
|
-
// Format results
|
|
2685
|
-
let resultText = `🧹 Test Artifact Cleanup Results\n${'='.repeat(50)}\n\n`;
|
|
2686
|
-
|
|
2687
|
-
if (dry_run) {
|
|
2688
|
-
resultText += `🔍 **DRY RUN MODE** - No actual deletion performed\n\n`;
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
resultText += `⏰ **Cleanup Configuration:**\n`;
|
|
2692
|
-
resultText += `- Artifact Types: ${artifact_types.join(', ')}\n`;
|
|
2693
|
-
resultText += `- Test Patterns: ${test_patterns.join(', ')}\n`;
|
|
2694
|
-
resultText += `- Max Age: ${max_age_hours} hours\n`;
|
|
2695
|
-
resultText += `- Cutoff Time: ${cutoffISO}\n`;
|
|
2696
|
-
resultText += `- Preserve Update Set Entries: ${preserve_update_set_entries ? '✅' : '❌'}\n\n`;
|
|
2697
|
-
|
|
2698
|
-
// Report findings by type
|
|
2699
|
-
['catalog_items', 'flows', 'users', 'requests'].forEach(type => {
|
|
2700
|
-
const found = cleanupResults.artifacts_found[type] || [];
|
|
2701
|
-
const deleted = cleanupResults.artifacts_deleted[type] || 0;
|
|
2702
|
-
|
|
2703
|
-
if (found.length > 0) {
|
|
2704
|
-
resultText += `📦 **${type.replace('_', ' ').toUpperCase()}:**\n`;
|
|
2705
|
-
resultText += ` ${dry_run ? 'Found' : 'Deleted'}: ${dry_run ? found.length : deleted}\n`;
|
|
2706
|
-
|
|
2707
|
-
if (dry_run && found.length > 0) {
|
|
2708
|
-
found.slice(0, 10).forEach((item: any) => {
|
|
2709
|
-
resultText += ` - ${item.name} (${item.sys_id}) - Created: ${item.sys_created_on}\n`;
|
|
2710
|
-
});
|
|
2711
|
-
if (found.length > 10) {
|
|
2712
|
-
resultText += ` ... and ${found.length - 10} more\n`;
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
resultText += '\n';
|
|
2716
|
-
}
|
|
2717
|
-
});
|
|
2718
|
-
|
|
2719
|
-
if (cleanupResults.update_set_entries_preserved > 0) {
|
|
2720
|
-
resultText += `📋 **Update Set Audit Trail Preserved:** ${cleanupResults.update_set_entries_preserved} entries\n\n`;
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
if (cleanupResults.errors.length > 0) {
|
|
2724
|
-
resultText += `❌ **Errors:**\n`;
|
|
2725
|
-
cleanupResults.errors.forEach(error => {
|
|
2726
|
-
resultText += ` - ${error}\n`;
|
|
2727
|
-
});
|
|
2728
|
-
resultText += '\n';
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
resultText += `✅ **${cleanupResults.summary}**\n\n`;
|
|
2732
|
-
|
|
2733
|
-
if (!dry_run && totalDeleted > 0) {
|
|
2734
|
-
resultText += `💡 **Note:** Update Set entries have been preserved as audit trail.\n`;
|
|
2735
|
-
resultText += `This shows that testing was performed and cleanup was completed.\n\n`;
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
|
-
if (dry_run) {
|
|
2739
|
-
resultText += `▶️ **Next Steps:**\n`;
|
|
2740
|
-
resultText += `1. Review the artifacts that would be deleted\n`;
|
|
2741
|
-
resultText += `2. Run again with dry_run: false to perform actual cleanup\n`;
|
|
2742
|
-
resultText += `3. Verify Update Set entries are preserved as intended\n`;
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
return {
|
|
2746
|
-
content: [{
|
|
2747
|
-
type: 'text',
|
|
2748
|
-
text: resultText
|
|
2749
|
-
}]
|
|
2750
|
-
};
|
|
2751
|
-
|
|
2752
|
-
} catch (error: any) {
|
|
2753
|
-
logger.error('Error during test artifact cleanup:', error);
|
|
2754
|
-
return {
|
|
2755
|
-
content: [{
|
|
2756
|
-
type: 'text',
|
|
2757
|
-
text: `❌ Test artifact cleanup failed\n\nError: ${error.message}\n\n` +
|
|
2758
|
-
`🔧 Troubleshooting:\n` +
|
|
2759
|
-
`1. Check ServiceNow connection and permissions\n` +
|
|
2760
|
-
`2. Verify test patterns are correct\n` +
|
|
2761
|
-
`3. Ensure artifacts exist and are accessible\n\n` +
|
|
2762
|
-
`Debug Info: ${JSON.stringify(cleanupResults, null, 2)}`
|
|
2763
|
-
}]
|
|
2764
|
-
};
|
|
2765
|
-
}
|
|
2766
|
-
}
|
|
2767
|
-
|
|
2768
|
-
private async cleanupTestCatalogItems(
|
|
2769
|
-
patterns: string[],
|
|
2770
|
-
cutoffTime: string,
|
|
2771
|
-
dryRun: boolean,
|
|
2772
|
-
results: any,
|
|
2773
|
-
updateSetFilter?: string
|
|
2774
|
-
) {
|
|
2775
|
-
try {
|
|
2776
|
-
// Build query for test catalog items
|
|
2777
|
-
const patternQueries = patterns.map(pattern => {
|
|
2778
|
-
if (pattern.includes('%')) {
|
|
2779
|
-
return `nameLIKE${pattern.replace(/%/g, '')}`;
|
|
2780
|
-
} else {
|
|
2781
|
-
return `name=${pattern}`;
|
|
2782
|
-
}
|
|
2783
|
-
});
|
|
2784
|
-
|
|
2785
|
-
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
|
|
2786
|
-
|
|
2787
|
-
const searchResult = await this.client.searchRecords('sc_cat_item', query, 100);
|
|
2788
|
-
|
|
2789
|
-
if (searchResult.success && searchResult.data?.result) {
|
|
2790
|
-
results.artifacts_found.catalog_items = searchResult.data.result.map((item: any) => ({
|
|
2791
|
-
sys_id: item.sys_id,
|
|
2792
|
-
name: item.name,
|
|
2793
|
-
sys_created_on: item.sys_created_on
|
|
2794
|
-
}));
|
|
2795
|
-
|
|
2796
|
-
if (!dryRun) {
|
|
2797
|
-
// Delete each catalog item
|
|
2798
|
-
for (const item of searchResult.data.result) {
|
|
2799
|
-
const deleteResult = await this.client.deleteRecord('sc_cat_item', item.sys_id);
|
|
2800
|
-
if (deleteResult.success) {
|
|
2801
|
-
results.artifacts_deleted.catalog_items++;
|
|
2802
|
-
} else {
|
|
2803
|
-
results.errors.push(`Failed to delete catalog item ${item.name}: ${deleteResult.error}`);
|
|
2804
|
-
}
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
} catch (error: any) {
|
|
2809
|
-
results.errors.push(`Error cleaning catalog items: ${error.message}`);
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
|
|
2813
|
-
private async cleanupTestUsers(
|
|
2814
|
-
patterns: string[],
|
|
2815
|
-
cutoffTime: string,
|
|
2816
|
-
dryRun: boolean,
|
|
2817
|
-
results: any,
|
|
2818
|
-
updateSetFilter?: string
|
|
2819
|
-
) {
|
|
2820
|
-
try {
|
|
2821
|
-
const patternQueries = patterns.map(pattern => {
|
|
2822
|
-
if (pattern.includes('%')) {
|
|
2823
|
-
return `user_nameLIKE${pattern.replace(/%/g, '')}^ORfirst_nameLIKE${pattern.replace(/%/g, '')}`;
|
|
2824
|
-
} else {
|
|
2825
|
-
return `user_name=${pattern}^ORfirst_name=${pattern}`;
|
|
2826
|
-
}
|
|
2827
|
-
});
|
|
2828
|
-
|
|
2829
|
-
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}^active=false`;
|
|
2830
|
-
|
|
2831
|
-
const searchResult = await this.client.searchRecords('sys_user', query, 100);
|
|
2832
|
-
|
|
2833
|
-
if (searchResult.success && searchResult.data?.result) {
|
|
2834
|
-
results.artifacts_found.users = searchResult.data.result.map((user: any) => ({
|
|
2835
|
-
sys_id: user.sys_id,
|
|
2836
|
-
name: user.user_name || user.name,
|
|
2837
|
-
sys_created_on: user.sys_created_on
|
|
2838
|
-
}));
|
|
2839
|
-
|
|
2840
|
-
if (!dryRun) {
|
|
2841
|
-
for (const user of searchResult.data.result) {
|
|
2842
|
-
const deleteResult = await this.client.deleteRecord('sys_user', user.sys_id);
|
|
2843
|
-
if (deleteResult.success) {
|
|
2844
|
-
results.artifacts_deleted.users++;
|
|
2845
|
-
} else {
|
|
2846
|
-
results.errors.push(`Failed to delete user ${user.user_name}: ${deleteResult.error}`);
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
}
|
|
2850
|
-
}
|
|
2851
|
-
} catch (error: any) {
|
|
2852
|
-
results.errors.push(`Error cleaning users: ${error.message}`);
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
|
|
2856
|
-
private async cleanupTestRequests(
|
|
2857
|
-
patterns: string[],
|
|
2858
|
-
cutoffTime: string,
|
|
2859
|
-
dryRun: boolean,
|
|
2860
|
-
results: any,
|
|
2861
|
-
updateSetFilter?: string
|
|
2862
|
-
) {
|
|
2863
|
-
try {
|
|
2864
|
-
const patternQueries = patterns.map(pattern => {
|
|
2865
|
-
if (pattern.includes('%')) {
|
|
2866
|
-
return `short_descriptionLIKE${pattern.replace(/%/g, '')}`;
|
|
2867
|
-
} else {
|
|
2868
|
-
return `short_description=${pattern}`;
|
|
2869
|
-
}
|
|
2870
|
-
});
|
|
2871
|
-
|
|
2872
|
-
const query = `(${patternQueries.join('^OR')})^sys_created_on<${cutoffTime}`;
|
|
2873
|
-
|
|
2874
|
-
const searchResult = await this.client.searchRecords('sc_request', query, 100);
|
|
2875
|
-
|
|
2876
|
-
if (searchResult.success && searchResult.data?.result) {
|
|
2877
|
-
results.artifacts_found.requests = searchResult.data.result.map((req: any) => ({
|
|
2878
|
-
sys_id: req.sys_id,
|
|
2879
|
-
name: req.number || req.short_description,
|
|
2880
|
-
sys_created_on: req.sys_created_on
|
|
2881
|
-
}));
|
|
2882
|
-
|
|
2883
|
-
if (!dryRun) {
|
|
2884
|
-
for (const request of searchResult.data.result) {
|
|
2885
|
-
// Cancel request instead of deleting (safer)
|
|
2886
|
-
const updateResult = await this.client.updateRecord('sc_request', request.sys_id, {
|
|
2887
|
-
state: '4', // Cancelled
|
|
2888
|
-
comments: 'Cancelled by test cleanup automation'
|
|
2889
|
-
});
|
|
2890
|
-
if (updateResult.success) {
|
|
2891
|
-
results.artifacts_deleted.requests++;
|
|
2892
|
-
} else {
|
|
2893
|
-
results.errors.push(`Failed to cancel request ${request.number}: ${updateResult.error}`);
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
}
|
|
2898
|
-
} catch (error: any) {
|
|
2899
|
-
results.errors.push(`Error cleaning requests: ${error.message}`);
|
|
2900
|
-
}
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
private getServiceNowUrl(): string {
|
|
2904
|
-
// This would need to get the instance URL from credentials
|
|
2905
|
-
return 'https://instance.service-now.com';
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
private async handleCreateUserGroup(args: any) {
|
|
2909
|
-
const { name, description, email, manager, parent, type, active = true } = args;
|
|
2910
|
-
|
|
2911
|
-
logger.info(`Creating user group: ${name}`);
|
|
2912
|
-
|
|
2913
|
-
try {
|
|
2914
|
-
// Check if group already exists
|
|
2915
|
-
const existingGroup = await this.client.get('/api/now/table/sys_user_group', {
|
|
2916
|
-
sysparm_query: `name=${name}`,
|
|
2917
|
-
sysparm_limit: 1
|
|
2918
|
-
});
|
|
2919
|
-
|
|
2920
|
-
if (existingGroup.result?.length > 0) {
|
|
2921
|
-
return {
|
|
2922
|
-
content: [{
|
|
2923
|
-
type: 'text',
|
|
2924
|
-
text: `✅ Group "${name}" already exists!\n\nSys ID: ${existingGroup.result[0].sys_id}\nManager: ${existingGroup.result[0].manager?.display_value || 'None'}\nType: ${existingGroup.result[0].type || 'Standard'}\nActive: ${existingGroup.result[0].active}\n\nNo action taken.`
|
|
2925
|
-
}]
|
|
2926
|
-
};
|
|
2927
|
-
}
|
|
2928
|
-
|
|
2929
|
-
// Prepare group data
|
|
2930
|
-
const groupData: any = {
|
|
2931
|
-
name,
|
|
2932
|
-
active: active.toString()
|
|
2933
|
-
};
|
|
2934
|
-
|
|
2935
|
-
if (description) groupData.description = description;
|
|
2936
|
-
if (email) groupData.email = email;
|
|
2937
|
-
if (type) groupData.type = type;
|
|
2938
|
-
|
|
2939
|
-
// Handle manager lookup
|
|
2940
|
-
if (manager) {
|
|
2941
|
-
const managerUser = await this.findUserBySysIdOrUsername(manager);
|
|
2942
|
-
if (managerUser) {
|
|
2943
|
-
groupData.manager = managerUser.sys_id;
|
|
2944
|
-
} else {
|
|
2945
|
-
logger.warn(`Manager "${manager}" not found, creating group without manager`);
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
// Handle parent group lookup
|
|
2950
|
-
if (parent) {
|
|
2951
|
-
const parentGroup = await this.findGroupBySysIdOrName(parent);
|
|
2952
|
-
if (parentGroup) {
|
|
2953
|
-
groupData.parent = parentGroup.sys_id;
|
|
2954
|
-
} else {
|
|
2955
|
-
logger.warn(`Parent group "${parent}" not found, creating group without parent`);
|
|
2956
|
-
}
|
|
2957
|
-
}
|
|
2958
|
-
|
|
2959
|
-
// Create the group
|
|
2960
|
-
const result = await this.client.post('/api/now/table/sys_user_group', groupData);
|
|
2961
|
-
|
|
2962
|
-
if (result.result) {
|
|
2963
|
-
const groupUrl = `${process.env.SNOW_INSTANCE ? `https://${process.env.SNOW_INSTANCE.replace(/\/$/, '')}.service-now.com` : ''}/sys_user_group.do?sys_id=${result.result.sys_id}`;
|
|
2964
|
-
|
|
2965
|
-
return {
|
|
2966
|
-
content: [{
|
|
2967
|
-
type: 'text',
|
|
2968
|
-
text: `✅ **User Group Created Successfully!**\n\n**Group Name:** ${result.result.name}\n**Sys ID:** ${result.result.sys_id}\n**Type:** ${result.result.type || 'Standard'}\n**Active:** ${result.result.active}\n${result.result.manager ? `**Manager:** ${result.result.manager.display_value}` : ''}\n${result.result.parent ? `**Parent Group:** ${result.result.parent.display_value}` : ''}\n\n**View Group:** ${groupUrl}\n\nYou can now assign users to this group using \`snow_assign_user_to_group\`.`
|
|
2969
|
-
}]
|
|
2970
|
-
};
|
|
2971
|
-
} else {
|
|
2972
|
-
throw new Error('Failed to create group - no result returned');
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
|
-
} catch (error) {
|
|
2976
|
-
return {
|
|
2977
|
-
content: [{
|
|
2978
|
-
type: 'text',
|
|
2979
|
-
text: `❌ **Failed to create user group**\n\nError: ${error instanceof Error ? error.message : String(error)}\n\nTip: Ensure you have the necessary permissions to create groups (admin or user_admin role).`
|
|
2980
|
-
}]
|
|
2981
|
-
};
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
|
|
2985
|
-
private async handleCreateUser(args: any) {
|
|
2986
|
-
const {
|
|
2987
|
-
user_name,
|
|
2988
|
-
first_name,
|
|
2989
|
-
last_name,
|
|
2990
|
-
email,
|
|
2991
|
-
department,
|
|
2992
|
-
manager,
|
|
2993
|
-
phone,
|
|
2994
|
-
title,
|
|
2995
|
-
location,
|
|
2996
|
-
active = true,
|
|
2997
|
-
password
|
|
2998
|
-
} = args;
|
|
2999
|
-
|
|
3000
|
-
logger.info(`Creating user: ${user_name}`);
|
|
3001
|
-
|
|
3002
|
-
try {
|
|
3003
|
-
// Check if user already exists
|
|
3004
|
-
const existingUser = await this.client.get('/api/now/table/sys_user', {
|
|
3005
|
-
sysparm_query: `user_name=${user_name}`,
|
|
3006
|
-
sysparm_limit: 1
|
|
3007
|
-
});
|
|
3008
|
-
|
|
3009
|
-
if (existingUser.result?.length > 0) {
|
|
3010
|
-
return {
|
|
3011
|
-
content: [{
|
|
3012
|
-
type: 'text',
|
|
3013
|
-
text: `✅ User "${user_name}" already exists!\n\nSys ID: ${existingUser.result[0].sys_id}\nName: ${existingUser.result[0].first_name} ${existingUser.result[0].last_name}\nEmail: ${existingUser.result[0].email}\nActive: ${existingUser.result[0].active}\n\nNo action taken.`
|
|
3014
|
-
}]
|
|
3015
|
-
};
|
|
3016
|
-
}
|
|
3017
|
-
|
|
3018
|
-
// Prepare user data
|
|
3019
|
-
const userData: any = {
|
|
3020
|
-
user_name,
|
|
3021
|
-
first_name,
|
|
3022
|
-
last_name,
|
|
3023
|
-
email,
|
|
3024
|
-
active: active.toString()
|
|
3025
|
-
};
|
|
3026
|
-
|
|
3027
|
-
if (phone) userData.phone = phone;
|
|
3028
|
-
if (title) userData.title = title;
|
|
3029
|
-
if (password) userData.user_password = password;
|
|
3030
|
-
|
|
3031
|
-
// Handle manager lookup
|
|
3032
|
-
if (manager) {
|
|
3033
|
-
const managerUser = await this.findUserBySysIdOrUsername(manager);
|
|
3034
|
-
if (managerUser) {
|
|
3035
|
-
userData.manager = managerUser.sys_id;
|
|
3036
|
-
} else {
|
|
3037
|
-
logger.warn(`Manager "${manager}" not found, creating user without manager`);
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
|
|
3041
|
-
// Handle department lookup
|
|
3042
|
-
if (department) {
|
|
3043
|
-
const dept = await this.findDepartment(department);
|
|
3044
|
-
if (dept) {
|
|
3045
|
-
userData.department = dept.sys_id;
|
|
3046
|
-
} else {
|
|
3047
|
-
logger.warn(`Department "${department}" not found, creating user without department`);
|
|
3048
|
-
}
|
|
3049
|
-
}
|
|
3050
|
-
|
|
3051
|
-
// Handle location lookup
|
|
3052
|
-
if (location) {
|
|
3053
|
-
const loc = await this.findLocation(location);
|
|
3054
|
-
if (loc) {
|
|
3055
|
-
userData.location = loc.sys_id;
|
|
3056
|
-
} else {
|
|
3057
|
-
logger.warn(`Location "${location}" not found, creating user without location`);
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
|
|
3061
|
-
// Create the user
|
|
3062
|
-
const result = await this.client.post('/api/now/table/sys_user', userData);
|
|
3063
|
-
|
|
3064
|
-
if (result.result) {
|
|
3065
|
-
const userUrl = `${process.env.SNOW_INSTANCE ? `https://${process.env.SNOW_INSTANCE.replace(/\/$/, '')}.service-now.com` : ''}/sys_user.do?sys_id=${result.result.sys_id}`;
|
|
3066
|
-
|
|
3067
|
-
return {
|
|
3068
|
-
content: [{
|
|
3069
|
-
type: 'text',
|
|
3070
|
-
text: `✅ **User Created Successfully!**\n\n**Username:** ${result.result.user_name}\n**Name:** ${result.result.first_name} ${result.result.last_name}\n**Email:** ${result.result.email}\n**Sys ID:** ${result.result.sys_id}\n**Active:** ${result.result.active}\n${result.result.manager ? `**Manager:** ${result.result.manager.display_value}` : ''}\n${result.result.department ? `**Department:** ${result.result.department.display_value}` : ''}\n${password ? '\n⚠️ **Note:** User will be required to change password on first login.' : ''}\n\n**View User:** ${userUrl}\n\nYou can now assign this user to groups using \`snow_assign_user_to_group\`.`
|
|
3071
|
-
}]
|
|
3072
|
-
};
|
|
3073
|
-
} else {
|
|
3074
|
-
throw new Error('Failed to create user - no result returned');
|
|
3075
|
-
}
|
|
3076
|
-
|
|
3077
|
-
} catch (error) {
|
|
3078
|
-
return {
|
|
3079
|
-
content: [{
|
|
3080
|
-
type: 'text',
|
|
3081
|
-
text: `❌ **Failed to create user**\n\nError: ${error instanceof Error ? error.message : String(error)}\n\nTip: Ensure you have the necessary permissions to create users (admin or user_admin role).`
|
|
3082
|
-
}]
|
|
3083
|
-
};
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
|
|
3087
|
-
private async handleAssignUserToGroup(args: any) {
|
|
3088
|
-
const { user, group } = args;
|
|
3089
|
-
|
|
3090
|
-
logger.info(`Assigning user ${user} to group ${group}`);
|
|
3091
|
-
|
|
3092
|
-
try {
|
|
3093
|
-
// Find the user
|
|
3094
|
-
const userRecord = await this.findUserBySysIdOrUsername(user);
|
|
3095
|
-
if (!userRecord) {
|
|
3096
|
-
return {
|
|
3097
|
-
content: [{
|
|
3098
|
-
type: 'text',
|
|
3099
|
-
text: `❌ User "${user}" not found. Please check the username or sys_id.`
|
|
3100
|
-
}]
|
|
3101
|
-
};
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
// Find the group
|
|
3105
|
-
const groupRecord = await this.findGroupBySysIdOrName(group);
|
|
3106
|
-
if (!groupRecord) {
|
|
3107
|
-
return {
|
|
3108
|
-
content: [{
|
|
3109
|
-
type: 'text',
|
|
3110
|
-
text: `❌ Group "${group}" not found. Please check the group name or sys_id.\n\nTip: You can create a new group using \`snow_create_user_group\`.`
|
|
3111
|
-
}]
|
|
3112
|
-
};
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
// Check if user is already in group
|
|
3116
|
-
const existingMembership = await this.client.get('/api/now/table/sys_user_grmember', {
|
|
3117
|
-
sysparm_query: `user=${userRecord.sys_id}^group=${groupRecord.sys_id}`,
|
|
3118
|
-
sysparm_limit: 1
|
|
3119
|
-
});
|
|
3120
|
-
|
|
3121
|
-
if (existingMembership.result?.length > 0) {
|
|
3122
|
-
return {
|
|
3123
|
-
content: [{
|
|
3124
|
-
type: 'text',
|
|
3125
|
-
text: `ℹ️ User "${userRecord.user_name}" is already a member of group "${groupRecord.name}".\n\nNo action taken.`
|
|
3126
|
-
}]
|
|
3127
|
-
};
|
|
3128
|
-
}
|
|
3129
|
-
|
|
3130
|
-
// Add user to group
|
|
3131
|
-
const result = await this.client.post('/api/now/table/sys_user_grmember', {
|
|
3132
|
-
user: userRecord.sys_id,
|
|
3133
|
-
group: groupRecord.sys_id
|
|
3134
|
-
});
|
|
3135
|
-
|
|
3136
|
-
if (result.result) {
|
|
3137
|
-
return {
|
|
3138
|
-
content: [{
|
|
3139
|
-
type: 'text',
|
|
3140
|
-
text: `✅ **User Added to Group Successfully!**\n\n**User:** ${userRecord.first_name} ${userRecord.last_name} (${userRecord.user_name})\n**Group:** ${groupRecord.name}\n**Membership Sys ID:** ${result.result.sys_id}\n\nThe user now has all permissions associated with the "${groupRecord.name}" group.`
|
|
3141
|
-
}]
|
|
3142
|
-
};
|
|
3143
|
-
} else {
|
|
3144
|
-
throw new Error('Failed to add user to group - no result returned');
|
|
3145
|
-
}
|
|
3146
|
-
|
|
3147
|
-
} catch (error) {
|
|
3148
|
-
return {
|
|
3149
|
-
content: [{
|
|
3150
|
-
type: 'text',
|
|
3151
|
-
text: `❌ **Failed to assign user to group**\n\nError: ${error instanceof Error ? error.message : String(error)}\n\nTip: Ensure you have the necessary permissions to manage group membership.`
|
|
3152
|
-
}]
|
|
3153
|
-
};
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
|
-
private async handleRemoveUserFromGroup(args: any) {
|
|
3158
|
-
const { user, group } = args;
|
|
3159
|
-
|
|
3160
|
-
logger.info(`Removing user ${user} from group ${group}`);
|
|
3161
|
-
|
|
3162
|
-
try {
|
|
3163
|
-
// Find the user
|
|
3164
|
-
const userRecord = await this.findUserBySysIdOrUsername(user);
|
|
3165
|
-
if (!userRecord) {
|
|
3166
|
-
return {
|
|
3167
|
-
content: [{
|
|
3168
|
-
type: 'text',
|
|
3169
|
-
text: `❌ User "${user}" not found. Please check the username or sys_id.`
|
|
3170
|
-
}]
|
|
3171
|
-
};
|
|
3172
|
-
}
|
|
3173
|
-
|
|
3174
|
-
// Find the group
|
|
3175
|
-
const groupRecord = await this.findGroupBySysIdOrName(group);
|
|
3176
|
-
if (!groupRecord) {
|
|
3177
|
-
return {
|
|
3178
|
-
content: [{
|
|
3179
|
-
type: 'text',
|
|
3180
|
-
text: `❌ Group "${group}" not found. Please check the group name or sys_id.`
|
|
3181
|
-
}]
|
|
3182
|
-
};
|
|
3183
|
-
}
|
|
3184
|
-
|
|
3185
|
-
// Find the membership record
|
|
3186
|
-
const membership = await this.client.get('/api/now/table/sys_user_grmember', {
|
|
3187
|
-
sysparm_query: `user=${userRecord.sys_id}^group=${groupRecord.sys_id}`,
|
|
3188
|
-
sysparm_limit: 1
|
|
3189
|
-
});
|
|
3190
|
-
|
|
3191
|
-
if (!membership.result || membership.result.length === 0) {
|
|
3192
|
-
return {
|
|
3193
|
-
content: [{
|
|
3194
|
-
type: 'text',
|
|
3195
|
-
text: `ℹ️ User "${userRecord.user_name}" is not a member of group "${groupRecord.name}".\n\nNo action taken.`
|
|
3196
|
-
}]
|
|
3197
|
-
};
|
|
3198
|
-
}
|
|
3199
|
-
|
|
3200
|
-
// Remove user from group
|
|
3201
|
-
await this.client.delete(`/api/now/table/sys_user_grmember/${membership.result[0].sys_id}`);
|
|
3202
|
-
|
|
3203
|
-
return {
|
|
3204
|
-
content: [{
|
|
3205
|
-
type: 'text',
|
|
3206
|
-
text: `✅ **User Removed from Group Successfully!**\n\n**User:** ${userRecord.first_name} ${userRecord.last_name} (${userRecord.user_name})\n**Group:** ${groupRecord.name}\n\nThe user no longer has permissions associated with the "${groupRecord.name}" group.`
|
|
3207
|
-
}]
|
|
3208
|
-
};
|
|
3209
|
-
|
|
3210
|
-
} catch (error) {
|
|
3211
|
-
return {
|
|
3212
|
-
content: [{
|
|
3213
|
-
type: 'text',
|
|
3214
|
-
text: `❌ **Failed to remove user from group**\n\nError: ${error instanceof Error ? error.message : String(error)}\n\nTip: Ensure you have the necessary permissions to manage group membership.`
|
|
3215
|
-
}]
|
|
3216
|
-
};
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
private async handleListGroupMembers(args: any) {
|
|
3221
|
-
const { group, active_only = true } = args;
|
|
3222
|
-
|
|
3223
|
-
logger.info(`Listing members of group: ${group}`);
|
|
3224
|
-
|
|
3225
|
-
try {
|
|
3226
|
-
// Find the group
|
|
3227
|
-
const groupRecord = await this.findGroupBySysIdOrName(group);
|
|
3228
|
-
if (!groupRecord) {
|
|
3229
|
-
return {
|
|
3230
|
-
content: [{
|
|
3231
|
-
type: 'text',
|
|
3232
|
-
text: `❌ Group "${group}" not found. Please check the group name or sys_id.`
|
|
3233
|
-
}]
|
|
3234
|
-
};
|
|
3235
|
-
}
|
|
3236
|
-
|
|
3237
|
-
// Get group members
|
|
3238
|
-
let query = `group=${groupRecord.sys_id}`;
|
|
3239
|
-
if (active_only) {
|
|
3240
|
-
query += '^user.active=true';
|
|
3241
|
-
}
|
|
3242
|
-
|
|
3243
|
-
const members = await this.client.get('/api/now/table/sys_user_grmember', {
|
|
3244
|
-
sysparm_query: query,
|
|
3245
|
-
sysparm_fields: 'user.user_name,user.first_name,user.last_name,user.email,user.title,user.active,user.sys_id',
|
|
3246
|
-
sysparm_limit: 100
|
|
3247
|
-
});
|
|
3248
|
-
|
|
3249
|
-
if (!members.result || members.result.length === 0) {
|
|
3250
|
-
return {
|
|
3251
|
-
content: [{
|
|
3252
|
-
type: 'text',
|
|
3253
|
-
text: `ℹ️ **Group "${groupRecord.name}" has no members.**\n\nYou can add users to this group using \`snow_assign_user_to_group\`.`
|
|
3254
|
-
}]
|
|
3255
|
-
};
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
|
-
// Format member list
|
|
3259
|
-
let response = `👥 **Members of "${groupRecord.name}" Group**\n\n`;
|
|
3260
|
-
response += `**Total Members:** ${members.result.length}${active_only ? ' (active only)' : ''}\n\n`;
|
|
3261
|
-
|
|
3262
|
-
members.result.forEach((member: any, index: number) => {
|
|
3263
|
-
const u = member.user;
|
|
3264
|
-
|
|
3265
|
-
// Defensive programming: check if user object exists and has required fields
|
|
3266
|
-
if (!u || !u.user_name) {
|
|
3267
|
-
logger.warn(`Group member ${index + 1} has invalid user data:`, member);
|
|
3268
|
-
response += `${index + 1}. **Invalid User Data** - ${member.sys_id || 'Unknown ID'}\n\n`;
|
|
3269
|
-
return;
|
|
3270
|
-
}
|
|
3271
|
-
|
|
3272
|
-
const firstName = u.first_name || 'N/A';
|
|
3273
|
-
const lastName = u.last_name || 'N/A';
|
|
3274
|
-
const email = u.email || 'N/A';
|
|
3275
|
-
const title = u.title || '';
|
|
3276
|
-
const active = u.active !== undefined ? u.active : 'Unknown';
|
|
3277
|
-
const sysId = u.sys_id || 'N/A';
|
|
3278
|
-
|
|
3279
|
-
response += `${index + 1}. **${firstName} ${lastName}** (${u.user_name})\n`;
|
|
3280
|
-
response += ` - Email: ${email}\n`;
|
|
3281
|
-
if (title) response += ` - Title: ${title}\n`;
|
|
3282
|
-
response += ` - Active: ${active}\n`;
|
|
3283
|
-
response += ` - Sys ID: ${sysId}\n\n`;
|
|
3284
|
-
});
|
|
3285
|
-
|
|
3286
|
-
if (members.result.length === 100) {
|
|
3287
|
-
response += `\n⚠️ **Note:** Results limited to 100 members. The group may have additional members.`;
|
|
3288
|
-
}
|
|
3289
|
-
|
|
3290
|
-
return {
|
|
3291
|
-
content: [{
|
|
3292
|
-
type: 'text',
|
|
3293
|
-
text: response
|
|
3294
|
-
}]
|
|
3295
|
-
};
|
|
3296
|
-
|
|
3297
|
-
} catch (error) {
|
|
3298
|
-
return {
|
|
3299
|
-
content: [{
|
|
3300
|
-
type: 'text',
|
|
3301
|
-
text: `❌ **Failed to list group members**\n\nError: ${error instanceof Error ? error.message : String(error)}`
|
|
3302
|
-
}]
|
|
3303
|
-
};
|
|
3304
|
-
}
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
// Helper methods
|
|
3308
|
-
private async findUserBySysIdOrUsername(identifier: string) {
|
|
3309
|
-
// First try as sys_id
|
|
3310
|
-
if (identifier.match(/^[a-f0-9]{32}$/)) {
|
|
3311
|
-
const result = await this.client.get(`/api/now/table/sys_user/${identifier}`);
|
|
3312
|
-
if (result.result) return result.result;
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
// Then try as username
|
|
3316
|
-
const result = await this.client.get('/api/now/table/sys_user', {
|
|
3317
|
-
sysparm_query: `user_name=${identifier}`,
|
|
3318
|
-
sysparm_limit: 1
|
|
3319
|
-
});
|
|
3320
|
-
|
|
3321
|
-
return result.result?.[0] || null;
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
private async findGroupBySysIdOrName(identifier: string) {
|
|
3325
|
-
// First try as sys_id
|
|
3326
|
-
if (identifier.match(/^[a-f0-9]{32}$/)) {
|
|
3327
|
-
const result = await this.client.get(`/api/now/table/sys_user_group/${identifier}`);
|
|
3328
|
-
if (result.result) return result.result;
|
|
3329
|
-
}
|
|
3330
|
-
|
|
3331
|
-
// Then try as name
|
|
3332
|
-
const result = await this.client.get('/api/now/table/sys_user_group', {
|
|
3333
|
-
sysparm_query: `name=${identifier}`,
|
|
3334
|
-
sysparm_limit: 1
|
|
3335
|
-
});
|
|
3336
|
-
|
|
3337
|
-
return result.result?.[0] || null;
|
|
3338
|
-
}
|
|
3339
|
-
|
|
3340
|
-
private async findDepartment(identifier: string) {
|
|
3341
|
-
// First try as sys_id
|
|
3342
|
-
if (identifier.match(/^[a-f0-9]{32}$/)) {
|
|
3343
|
-
const result = await this.client.get(`/api/now/table/cmn_department/${identifier}`);
|
|
3344
|
-
if (result.result) return result.result;
|
|
3345
|
-
}
|
|
3346
|
-
|
|
3347
|
-
// Then try as name
|
|
3348
|
-
const result = await this.client.get('/api/now/table/cmn_department', {
|
|
3349
|
-
sysparm_query: `name=${identifier}`,
|
|
3350
|
-
sysparm_limit: 1
|
|
3351
|
-
});
|
|
3352
|
-
|
|
3353
|
-
return result.result?.[0] || null;
|
|
3354
|
-
}
|
|
3355
|
-
|
|
3356
|
-
private async findLocation(identifier: string) {
|
|
3357
|
-
// First try as sys_id
|
|
3358
|
-
if (identifier.match(/^[a-f0-9]{32}$/)) {
|
|
3359
|
-
const result = await this.client.get(`/api/now/table/cmn_location/${identifier}`);
|
|
3360
|
-
if (result.result) return result.result;
|
|
3361
|
-
}
|
|
3362
|
-
|
|
3363
|
-
// Then try as name
|
|
3364
|
-
const result = await this.client.get('/api/now/table/cmn_location', {
|
|
3365
|
-
sysparm_query: `name=${identifier}`,
|
|
3366
|
-
sysparm_limit: 1
|
|
3367
|
-
});
|
|
3368
|
-
|
|
3369
|
-
return result.result?.[0] || null;
|
|
3370
|
-
}
|
|
3371
|
-
|
|
3372
|
-
async run() {
|
|
3373
|
-
const transport = new StdioServerTransport();
|
|
3374
|
-
await this.server.connect(transport);
|
|
3375
|
-
logger.info('ServiceNow Operations MCP Server started');
|
|
3376
|
-
}
|
|
3377
|
-
}
|
|
3378
|
-
|
|
3379
|
-
// Start the server
|
|
3380
|
-
const server = new ServiceNowOperationsMCP();
|
|
3381
|
-
server.run().catch((error) => {
|
|
3382
|
-
logger.error('Failed to start ServiceNow Operations MCP server:', error);
|
|
3383
|
-
process.exit(1);
|
|
3384
|
-
});
|