servicenow-mcp-server 2.1.0

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.
Files changed (52) hide show
  1. package/.claude/settings.local.json +70 -0
  2. package/CLAUDE.md +777 -0
  3. package/LICENSE +21 -0
  4. package/README.md +562 -0
  5. package/assets/logo.svg +385 -0
  6. package/config/servicenow-instances.json.example +28 -0
  7. package/docs/403_TROUBLESHOOTING.md +329 -0
  8. package/docs/API_REFERENCE.md +1142 -0
  9. package/docs/APPLICATION_SCOPE_VALIDATION.md +681 -0
  10. package/docs/CLAUDE_DESKTOP_SETUP.md +373 -0
  11. package/docs/CONVENIENCE_TOOLS.md +601 -0
  12. package/docs/CONVENIENCE_TOOLS_SUMMARY.md +371 -0
  13. package/docs/FLOW_DESIGNER_GUIDE.md +1021 -0
  14. package/docs/IMPLEMENTATION_COMPLETE.md +165 -0
  15. package/docs/INSTANCE_SWITCHING_GUIDE.md +219 -0
  16. package/docs/MULTI_INSTANCE_CONFIGURATION.md +185 -0
  17. package/docs/NATURAL_LANGUAGE_SEARCH_IMPLEMENTATION.md +221 -0
  18. package/docs/PUPPETEER_INTEGRATION_PROPOSAL.md +1322 -0
  19. package/docs/QUICK_REFERENCE.md +395 -0
  20. package/docs/README.md +75 -0
  21. package/docs/RESOURCES_ARCHITECTURE.md +392 -0
  22. package/docs/RESOURCES_IMPLEMENTATION.md +276 -0
  23. package/docs/RESOURCES_SUMMARY.md +104 -0
  24. package/docs/SETUP_GUIDE.md +104 -0
  25. package/docs/UI_OPERATIONS_ARCHITECTURE.md +1219 -0
  26. package/docs/UI_OPERATIONS_DECISION_MATRIX.md +542 -0
  27. package/docs/UI_OPERATIONS_SUMMARY.md +507 -0
  28. package/docs/UPDATE_SET_VALIDATION.md +598 -0
  29. package/docs/UPDATE_SET_VALIDATION_SUMMARY.md +209 -0
  30. package/docs/VALIDATION_SUMMARY.md +479 -0
  31. package/jest.config.js +24 -0
  32. package/package.json +61 -0
  33. package/scripts/background_script_2025-09-29T20-19-35-101Z.js +23 -0
  34. package/scripts/link_ui_policy_actions_2025-09-29T20-17-15-218Z.js +90 -0
  35. package/scripts/set_update_set_Integration_Governance_Framework_2025-09-29T19-47-06-790Z.js +30 -0
  36. package/scripts/set_update_set_Integration_Governance_Framework_2025-09-29T19-59-33-152Z.js +30 -0
  37. package/scripts/set_update_set_current_2025-09-29T20-16-59-675Z.js +24 -0
  38. package/scripts/test_sys_dictionary_403.js +85 -0
  39. package/setup/setup-report.json +5313 -0
  40. package/src/config/comprehensive-table-definitions.json +2575 -0
  41. package/src/config/instance-config.json +4693 -0
  42. package/src/config/prompts.md +59 -0
  43. package/src/config/table-definitions.json +4681 -0
  44. package/src/config-manager.js +146 -0
  45. package/src/mcp-server-consolidated.js +2894 -0
  46. package/src/natural-language.js +472 -0
  47. package/src/resources.js +326 -0
  48. package/src/script-sync.js +428 -0
  49. package/src/server.js +125 -0
  50. package/src/servicenow-client.js +1625 -0
  51. package/src/stdio-server.js +52 -0
  52. package/start-mcp.sh +7 -0
@@ -0,0 +1,1625 @@
1
+ /**
2
+ * ServiceNow MCP Server - REST API Client
3
+ *
4
+ * Copyright (c) 2025 Happy Technologies LLC
5
+ * Licensed under the MIT License - see LICENSE file for details
6
+ */
7
+
8
+ import axios from 'axios';
9
+
10
+ export class ServiceNowClient {
11
+ constructor(instanceUrl, username, password) {
12
+ this.currentInstanceName = 'default';
13
+ this.setInstance(instanceUrl, username, password);
14
+ this.progressCallback = null; // Callback for progress notifications
15
+ }
16
+
17
+ /**
18
+ * Set progress callback for notifications
19
+ * @param {Function} callback - Function to call with progress updates
20
+ */
21
+ setProgressCallback(callback) {
22
+ this.progressCallback = callback;
23
+ }
24
+
25
+ /**
26
+ * Send progress notification
27
+ * @param {string} message - Progress message
28
+ */
29
+ notifyProgress(message) {
30
+ if (this.progressCallback) {
31
+ this.progressCallback(message);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Switch to a different ServiceNow instance
37
+ * @param {string} instanceUrl - Instance URL
38
+ * @param {string} username - Username
39
+ * @param {string} password - Password
40
+ * @param {string} instanceName - Optional instance name for tracking
41
+ */
42
+ setInstance(instanceUrl, username, password, instanceName = null) {
43
+ this.instanceUrl = instanceUrl.replace(/\/$/, ''); // Remove trailing slash
44
+ this.auth = Buffer.from(`${username}:${password}`).toString('base64');
45
+
46
+ if (instanceName) {
47
+ this.currentInstanceName = instanceName;
48
+ }
49
+
50
+ this.client = axios.create({
51
+ baseURL: this.instanceUrl,
52
+ headers: {
53
+ 'Authorization': `Basic ${this.auth}`,
54
+ 'Content-Type': 'application/json',
55
+ 'Accept': 'application/json'
56
+ }
57
+ });
58
+ }
59
+
60
+ /**
61
+ * Get current instance information
62
+ * @returns {object} Current instance details
63
+ */
64
+ getCurrentInstance() {
65
+ return {
66
+ name: this.currentInstanceName,
67
+ url: this.instanceUrl
68
+ };
69
+ }
70
+
71
+ // Generic table operations
72
+ async getRecords(table, query = {}) {
73
+ const params = new URLSearchParams();
74
+ if (query.sysparm_query) params.append('sysparm_query', query.sysparm_query);
75
+ if (query.sysparm_limit) params.append('sysparm_limit', query.sysparm_limit);
76
+ if (query.sysparm_fields) params.append('sysparm_fields', query.sysparm_fields);
77
+
78
+ const response = await this.client.get(`/api/now/table/${table}?${params}`);
79
+ return response.data.result;
80
+ }
81
+
82
+ async getRecord(table, sysId, queryParams = {}) {
83
+ const params = new URLSearchParams();
84
+ if (queryParams.sysparm_fields) params.append('sysparm_fields', queryParams.sysparm_fields);
85
+ if (queryParams.sysparm_display_value) params.append('sysparm_display_value', queryParams.sysparm_display_value);
86
+ if (queryParams.sysparm_exclude_reference_link) params.append('sysparm_exclude_reference_link', queryParams.sysparm_exclude_reference_link);
87
+
88
+ const queryString = params.toString();
89
+ const url = queryString ? `/api/now/table/${table}/${sysId}?${queryString}` : `/api/now/table/${table}/${sysId}`;
90
+
91
+ const response = await this.client.get(url);
92
+ return response.data.result;
93
+ }
94
+
95
+ async createRecord(table, data) {
96
+ const response = await this.client.post(`/api/now/table/${table}`, data);
97
+ return response.data.result;
98
+ }
99
+
100
+ async updateRecord(table, sysId, data) {
101
+ const response = await this.client.put(`/api/now/table/${table}/${sysId}`, data);
102
+ return response.data.result;
103
+ }
104
+
105
+ async deleteRecord(table, sysId) {
106
+ await this.client.delete(`/api/now/table/${table}/${sysId}`);
107
+ return { success: true };
108
+ }
109
+
110
+ // Update set management via UI API endpoint
111
+ async setCurrentUpdateSet(updateSetSysId) {
112
+ try {
113
+ // First, get the update set name
114
+ const updateSet = await this.getRecord('sys_update_set', updateSetSysId);
115
+
116
+ // Create axios client with UI session
117
+ const axiosWithCookies = axios.create({
118
+ baseURL: this.instanceUrl,
119
+ headers: {
120
+ 'Authorization': `Basic ${this.auth}`,
121
+ 'Content-Type': 'application/json',
122
+ 'Accept': 'application/json',
123
+ 'User-Agent': 'ServiceNow-MCP-Client/2.0'
124
+ },
125
+ withCredentials: true,
126
+ maxRedirects: 5
127
+ });
128
+
129
+ // Establish session first
130
+ await axiosWithCookies.get('/', {
131
+ headers: { 'Accept': 'text/html' }
132
+ });
133
+
134
+ // Set the update set via UI API
135
+ const response = await axiosWithCookies.put(
136
+ '/api/now/ui/concoursepicker/updateset',
137
+ {
138
+ name: updateSet.name,
139
+ sysId: updateSetSysId
140
+ }
141
+ );
142
+
143
+ return {
144
+ success: true,
145
+ update_set: updateSet.name,
146
+ sys_id: updateSetSysId,
147
+ response: response.data
148
+ };
149
+ } catch (error) {
150
+ // If UI API fails, fall back to sys_trigger method
151
+ console.log('UI API failed, falling back to sys_trigger...');
152
+
153
+ const updateSet = await this.getRecord('sys_update_set', updateSetSysId);
154
+ const script = `// Update user preference for current update set
155
+ var updateSetId = '${updateSetSysId}';
156
+
157
+ // Delete existing preference
158
+ var delGR = new GlideRecord('sys_user_preference');
159
+ delGR.addQuery('user', gs.getUserID());
160
+ delGR.addQuery('name', 'sys_update_set');
161
+ delGR.query();
162
+ if (delGR.next()) {
163
+ delGR.deleteRecord();
164
+ }
165
+
166
+ // Create new preference
167
+ var gr = new GlideRecord('sys_user_preference');
168
+ gr.initialize();
169
+ gr.user = gs.getUserID();
170
+ gr.name = 'sys_update_set';
171
+ gr.value = updateSetId;
172
+ gr.insert();
173
+
174
+ gs.info('✅ Update set changed to: ${updateSet.name}');`;
175
+
176
+ const result = await this.executeScriptViaTrigger(script, `Set update set to: ${updateSet.name}`, true);
177
+ return {
178
+ success: true,
179
+ update_set: updateSet.name,
180
+ sys_id: updateSetSysId,
181
+ method: 'sys_trigger',
182
+ trigger_details: result
183
+ };
184
+ }
185
+ }
186
+
187
+ async getCurrentUpdateSet() {
188
+ // Get the current update set preference
189
+ const response = await this.client.get(`/api/now/ui/preferences/sys_update_set`);
190
+ return response.data;
191
+ }
192
+
193
+ async listUpdateSets(query = {}) {
194
+ // List available update sets
195
+ return this.getRecords('sys_update_set', query);
196
+ }
197
+
198
+ async setCurrentApplication(appSysId) {
199
+ const startTime = Date.now();
200
+
201
+ try {
202
+ // Validate input
203
+ if (!appSysId) {
204
+ throw new Error('app_sys_id is required');
205
+ }
206
+
207
+ // Validate sys_id format (32-character hex string)
208
+ if (!/^[0-9a-f]{32}$/i.test(appSysId)) {
209
+ throw new Error(`Invalid sys_id format: ${appSysId}. Must be a 32-character hexadecimal string.`);
210
+ }
211
+
212
+ // Get previous application scope (for rollback information)
213
+ let previousScope = null;
214
+ try {
215
+ const prefResponse = await this.client.get('/api/now/ui/preferences/apps.current');
216
+ if (prefResponse.data && prefResponse.data.result) {
217
+ previousScope = {
218
+ sys_id: prefResponse.data.result.value || null,
219
+ name: prefResponse.data.result.display_value || null
220
+ };
221
+ }
222
+ } catch (prefError) {
223
+ // Previous scope query failed - not critical, continue
224
+ console.log('Could not retrieve previous scope:', prefError.message);
225
+ }
226
+
227
+ // Get application details
228
+ let app;
229
+ try {
230
+ app = await this.getRecord('sys_app', appSysId);
231
+ } catch (appError) {
232
+ if (appError.response && appError.response.status === 404) {
233
+ throw new Error(`Application not found with sys_id: ${appSysId}. Please verify the sys_id is correct.`);
234
+ }
235
+ throw appError;
236
+ }
237
+
238
+ // Create axios client with cookie jar
239
+ const axiosWithCookies = axios.create({
240
+ baseURL: this.instanceUrl,
241
+ headers: {
242
+ 'Authorization': `Basic ${this.auth}`,
243
+ 'Content-Type': 'application/json',
244
+ 'User-Agent': 'ServiceNow-MCP-Client/2.0'
245
+ },
246
+ withCredentials: true,
247
+ maxRedirects: 5
248
+ });
249
+
250
+ // Establish session first
251
+ await axiosWithCookies.get('/', {
252
+ headers: { 'Accept': 'text/html' }
253
+ });
254
+
255
+ // Set the application via UI API
256
+ const response = await axiosWithCookies.put(
257
+ '/api/now/ui/concoursepicker/application',
258
+ {
259
+ app_id: appSysId
260
+ }
261
+ );
262
+
263
+ // Verify the scope was set correctly
264
+ let verified = false;
265
+ let verificationError = null;
266
+ try {
267
+ await new Promise(resolve => setTimeout(resolve, 500)); // Wait for preference to update
268
+ const verifyResponse = await this.client.get('/api/now/ui/preferences/apps.current');
269
+ if (verifyResponse.data && verifyResponse.data.result) {
270
+ const currentAppId = verifyResponse.data.result.value;
271
+ verified = (currentAppId === appSysId);
272
+ if (!verified) {
273
+ verificationError = `Verification failed: Current app is ${currentAppId}, expected ${appSysId}`;
274
+ }
275
+ }
276
+ } catch (verifyError) {
277
+ verificationError = `Verification query failed: ${verifyError.message}`;
278
+ }
279
+
280
+ const executionTime = Date.now() - startTime;
281
+
282
+ const result = {
283
+ success: true,
284
+ application: app.name,
285
+ scope: app.scope || 'global',
286
+ sys_id: appSysId,
287
+ previous_scope: previousScope,
288
+ verified: verified,
289
+ verification_error: verificationError,
290
+ timestamp: new Date().toISOString(),
291
+ execution_time_ms: executionTime,
292
+ method: 'ui_api',
293
+ endpoint: '/api/now/ui/concoursepicker/application',
294
+ response: response.data
295
+ };
296
+
297
+ // Add warnings if applicable
298
+ result.warnings = [];
299
+ if (!verified) {
300
+ result.warnings.push('Could not verify scope change - please check ServiceNow UI');
301
+ }
302
+ if (verificationError) {
303
+ result.warnings.push(verificationError);
304
+ }
305
+
306
+ return result;
307
+ } catch (error) {
308
+ const executionTime = Date.now() - startTime;
309
+
310
+ // Enhanced error messages based on error type
311
+ let errorMessage = error.message;
312
+
313
+ if (error.response) {
314
+ const status = error.response.status;
315
+ if (status === 401) {
316
+ errorMessage = 'Authentication failed. Please check your credentials.';
317
+ } else if (status === 403) {
318
+ errorMessage = `Access denied. Please verify:\n1. You have admin or developer role\n2. You have access to the application\n3. The application is active`;
319
+ } else if (status === 404) {
320
+ errorMessage = `Application not found with sys_id: ${appSysId}`;
321
+ } else if (status >= 500) {
322
+ errorMessage = `ServiceNow server error (${status}). Please try again later.`;
323
+ }
324
+ }
325
+
326
+ console.error('Failed to set current application:', errorMessage);
327
+
328
+ const enhancedError = new Error(`Failed to set current application: ${errorMessage}`);
329
+ enhancedError.execution_time_ms = executionTime;
330
+ enhancedError.app_sys_id = appSysId;
331
+ enhancedError.original_error = error;
332
+
333
+ throw enhancedError;
334
+ }
335
+ }
336
+
337
+ // Incident-specific methods
338
+ async getIncidents(query = {}) {
339
+ return this.getRecords('incident', query);
340
+ }
341
+
342
+ async getIncidentByNumber(number) {
343
+ const incidents = await this.getRecords('incident', {
344
+ sysparm_query: `number=${number}`,
345
+ sysparm_limit: 1
346
+ });
347
+ return incidents[0] || null;
348
+ }
349
+
350
+ async createIncident(data) {
351
+ return this.createRecord('incident', data);
352
+ }
353
+
354
+ async updateIncident(sysId, data) {
355
+ return this.updateRecord('incident', sysId, data);
356
+ }
357
+
358
+ // User management
359
+ async getUsers(query = {}) {
360
+ return this.getRecords('sys_user', query);
361
+ }
362
+
363
+ async createUser(data) {
364
+ return this.createRecord('sys_user', data);
365
+ }
366
+
367
+ async updateUser(sysId, data) {
368
+ return this.updateRecord('sys_user', sysId, data);
369
+ }
370
+
371
+ // Catalog items
372
+ async getCatalogItems(query = {}) {
373
+ return this.getRecords('sc_cat_item', query);
374
+ }
375
+
376
+ async createCatalogItem(data) {
377
+ return this.createRecord('sc_cat_item', data);
378
+ }
379
+
380
+ // Change requests
381
+ async getChangeRequests(query = {}) {
382
+ return this.getRecords('change_request', query);
383
+ }
384
+
385
+ async getChangeRequestByNumber(number) {
386
+ const changes = await this.getRecords('change_request', {
387
+ sysparm_query: `number=${number}`,
388
+ sysparm_limit: 1
389
+ });
390
+ return changes[0] || null;
391
+ }
392
+
393
+ async createChangeRequest(data) {
394
+ return this.createRecord('change_request', data);
395
+ }
396
+
397
+ async updateChangeRequest(sysId, data) {
398
+ return this.updateRecord('change_request', sysId, data);
399
+ }
400
+
401
+ // Problems
402
+ async getProblems(query = {}) {
403
+ return this.getRecords('problem', query);
404
+ }
405
+
406
+ async getProblemByNumber(number) {
407
+ const problems = await this.getRecords('problem', {
408
+ sysparm_query: `number=${number}`,
409
+ sysparm_limit: 1
410
+ });
411
+ return problems[0] || null;
412
+ }
413
+
414
+ async createProblem(data) {
415
+ return this.createRecord('problem', data);
416
+ }
417
+
418
+ async updateProblem(sysId, data) {
419
+ return this.updateRecord('problem', sysId, data);
420
+ }
421
+
422
+ // Service Requests
423
+ async getServiceRequests(query = {}) {
424
+ return this.getRecords('sc_request', query);
425
+ }
426
+
427
+ async createServiceRequest(data) {
428
+ return this.createRecord('sc_request', data);
429
+ }
430
+
431
+ async updateServiceRequest(sysId, data) {
432
+ return this.updateRecord('sc_request', sysId, data);
433
+ }
434
+
435
+ // Groups
436
+ async getGroups(query = {}) {
437
+ return this.getRecords('sys_user_group', query);
438
+ }
439
+
440
+ async createGroup(data) {
441
+ return this.createRecord('sys_user_group', data);
442
+ }
443
+
444
+ async updateGroup(sysId, data) {
445
+ return this.updateRecord('sys_user_group', sysId, data);
446
+ }
447
+
448
+ // Group membership
449
+ async addUserToGroup(userId, groupId) {
450
+ return this.createRecord('sys_user_grmember', {
451
+ user: userId,
452
+ group: groupId
453
+ });
454
+ }
455
+
456
+ async removeUserFromGroup(userId, groupId) {
457
+ const members = await this.getRecords('sys_user_grmember', {
458
+ sysparm_query: `user=${userId}^group=${groupId}`,
459
+ sysparm_limit: 1
460
+ });
461
+ if (members[0]) {
462
+ await this.deleteRecord('sys_user_grmember', members[0].sys_id);
463
+ }
464
+ return { success: true };
465
+ }
466
+
467
+ // Knowledge Base
468
+ async getKnowledgeBases(query = {}) {
469
+ return this.getRecords('kb_knowledge_base', query);
470
+ }
471
+
472
+ async createKnowledgeBase(data) {
473
+ return this.createRecord('kb_knowledge_base', data);
474
+ }
475
+
476
+ // Knowledge Articles
477
+ async getKnowledgeArticles(query = {}) {
478
+ return this.getRecords('kb_knowledge', query);
479
+ }
480
+
481
+ async createKnowledgeArticle(data) {
482
+ return this.createRecord('kb_knowledge', data);
483
+ }
484
+
485
+ async updateKnowledgeArticle(sysId, data) {
486
+ return this.updateRecord('kb_knowledge', sysId, data);
487
+ }
488
+
489
+ // Catalog Categories
490
+ async getCatalogCategories(query = {}) {
491
+ return this.getRecords('sc_category', query);
492
+ }
493
+
494
+ async createCatalogCategory(data) {
495
+ return this.createRecord('sc_category', data);
496
+ }
497
+
498
+ async updateCatalogCategory(sysId, data) {
499
+ return this.updateRecord('sc_category', sysId, data);
500
+ }
501
+
502
+ // Add comments/work notes to any table
503
+ async addComment(table, recordId, comment, isWorkNote = false) {
504
+ const field = isWorkNote ? 'work_notes' : 'comments';
505
+ const updateData = { [field]: comment };
506
+ return this.updateRecord(table, recordId, updateData);
507
+ }
508
+
509
+ // Generic search across tables
510
+ async searchRecords(table, searchTerm, fields = [], limit = 10) {
511
+ const searchQuery = fields.length > 0
512
+ ? fields.map(field => `${field}CONTAINS${searchTerm}`).join('^OR')
513
+ : `short_descriptionCONTAINS${searchTerm}^ORdescriptionCONTAINS${searchTerm}`;
514
+
515
+ return this.getRecords(table, {
516
+ sysparm_query: searchQuery,
517
+ sysparm_limit: limit
518
+ });
519
+ }
520
+
521
+ // Configuration Items (CMDB)
522
+ async getConfigurationItems(query = {}) {
523
+ return this.getRecords('cmdb_ci', query);
524
+ }
525
+
526
+ async getConfigurationItem(sysId) {
527
+ return this.getRecord('cmdb_ci', sysId);
528
+ }
529
+
530
+ async createConfigurationItem(data) {
531
+ return this.createRecord('cmdb_ci', data);
532
+ }
533
+
534
+ async updateConfigurationItem(sysId, data) {
535
+ return this.updateRecord('cmdb_ci', sysId, data);
536
+ }
537
+
538
+ // Business Rules
539
+ async getBusinessRules(query = {}) {
540
+ return this.getRecords('sys_script', query);
541
+ }
542
+
543
+ async createBusinessRule(data) {
544
+ return this.createRecord('sys_script', data);
545
+ }
546
+
547
+ async updateBusinessRule(sysId, data) {
548
+ return this.updateRecord('sys_script', sysId, data);
549
+ }
550
+
551
+ // Update Sets
552
+ async getUpdateSets(query = {}) {
553
+ return this.getRecords('sys_update_set', query);
554
+ }
555
+
556
+ async createUpdateSet(data) {
557
+ return this.createRecord('sys_update_set', data);
558
+ }
559
+
560
+ async updateUpdateSet(sysId, data) {
561
+ return this.updateRecord('sys_update_set', sysId, data);
562
+ }
563
+
564
+ // Workflows
565
+ async getWorkflows(query = {}) {
566
+ return this.getRecords('wf_workflow', query);
567
+ }
568
+
569
+ async createWorkflow(data) {
570
+ return this.createRecord('wf_workflow', data);
571
+ }
572
+
573
+ // Attachments
574
+ async getAttachments(tableId, recordId) {
575
+ return this.getRecords('sys_attachment', {
576
+ sysparm_query: `table_name=${tableId}^table_sys_id=${recordId}`
577
+ });
578
+ }
579
+
580
+ async createAttachment(tableName, recordId, fileName, contentType, data) {
581
+ const formData = new FormData();
582
+ formData.append('table_name', tableName);
583
+ formData.append('table_sys_id', recordId);
584
+ formData.append('file_name', fileName);
585
+ formData.append('content_type', contentType);
586
+ formData.append('content', data);
587
+
588
+ const response = await this.client.post('/api/now/attachment/file', formData, {
589
+ headers: {
590
+ 'Content-Type': 'multipart/form-data'
591
+ }
592
+ });
593
+ return response.data.result;
594
+ }
595
+
596
+ // SLA Management
597
+ async getSLAs(query = {}) {
598
+ return this.getRecords('contract_sla', query);
599
+ }
600
+
601
+ async getSLADefinitions(query = {}) {
602
+ return this.getRecords('sla_definition', query);
603
+ }
604
+
605
+ // Task Assignment
606
+ async assignTask(table, recordId, assignedTo, assignmentGroup = null) {
607
+ const updateData = { assigned_to: assignedTo };
608
+ if (assignmentGroup) {
609
+ updateData.assignment_group = assignmentGroup;
610
+ }
611
+ return this.updateRecord(table, recordId, updateData);
612
+ }
613
+
614
+ // Reports
615
+ async getReports(query = {}) {
616
+ return this.getRecords('sys_report', query);
617
+ }
618
+
619
+ async createReport(data) {
620
+ return this.createRecord('sys_report', data);
621
+ }
622
+
623
+ // Execute script via sys_trigger (scheduled job that runs immediately)
624
+ async executeScriptViaTrigger(script, description = 'MCP Script Execution', autoDelete = true) {
625
+ try {
626
+ // Calculate next action time (1 second from now)
627
+ const now = new Date();
628
+ const nextAction = new Date(now.getTime() + 1000); // 1 second from now
629
+
630
+ // Format: YYYY-MM-DD HH:MM:SS
631
+ const formatDateTime = (date) => {
632
+ const pad = (n) => n.toString().padStart(2, '0');
633
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
634
+ };
635
+
636
+ // Wrap script with auto-delete logic if requested
637
+ let finalScript = script;
638
+ let triggerSysId = null;
639
+ if (autoDelete) {
640
+ // We'll set the sys_id after creation, then update the script
641
+ finalScript = script; // Use original script for now
642
+ }
643
+
644
+ // Create sys_trigger record
645
+ const trigger = await this.createRecord('sys_trigger', {
646
+ name: `MCP_Script_${Date.now()}`,
647
+ script: finalScript,
648
+ next_action: formatDateTime(nextAction),
649
+ trigger_type: '0', // Run once
650
+ state: '0', // Ready state
651
+ description: description || 'Automated script execution via MCP'
652
+ });
653
+
654
+ // If auto-delete requested, update script with self-delete logic
655
+ if (autoDelete) {
656
+ const scriptWithDelete = `
657
+ // Auto-generated MCP script trigger
658
+ try {
659
+ ${script}
660
+ } finally {
661
+ // Auto-delete this trigger after execution
662
+ var triggerGR = new GlideRecord('sys_trigger');
663
+ if (triggerGR.get('${trigger.sys_id}')) {
664
+ triggerGR.deleteRecord();
665
+ gs.info('MCP: Auto-deleted trigger ${trigger.sys_id}');
666
+ }
667
+ }`;
668
+
669
+ await this.updateRecord('sys_trigger', trigger.sys_id, {
670
+ script: scriptWithDelete
671
+ });
672
+ }
673
+
674
+ return {
675
+ success: true,
676
+ trigger_sys_id: trigger.sys_id,
677
+ trigger_name: trigger.name,
678
+ next_action: formatDateTime(nextAction),
679
+ auto_delete: autoDelete,
680
+ message: `Script scheduled to run at ${formatDateTime(nextAction)}. ${autoDelete ? 'Trigger will auto-delete after execution.' : 'Trigger will remain after execution.'}`
681
+ };
682
+ } catch (error) {
683
+ throw new Error(`Failed to create script trigger: ${error.message}`);
684
+ }
685
+ }
686
+
687
+ // Background script execution via UI endpoint (NOT WORKING - requires interactive session)
688
+ // NOTE: /sys.scripts.do endpoint requires interactive browser session with cookies
689
+ // from login.do - Basic Auth is not sufficient. Always fails with X-Is-Logged-In: false
690
+ // Use executeScriptViaTrigger() instead.
691
+ async executeBackgroundScript(script, scope = 'global') {
692
+ throw new Error('Direct UI script execution not supported - use sys_trigger method instead');
693
+ }
694
+
695
+ // Batch operations
696
+ async batchCreate(operations, transaction = true, reportProgress = true) {
697
+ const results = {
698
+ success: true,
699
+ created_count: 0,
700
+ sys_ids: {},
701
+ errors: [],
702
+ execution_time_ms: 0
703
+ };
704
+
705
+ const startTime = Date.now();
706
+ const total = operations.length;
707
+
708
+ // Determine progress reporting frequency
709
+ const shouldReport = (index) => {
710
+ if (!reportProgress) return false;
711
+ if (total <= 10) return true; // Report every item for small batches
712
+ if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1; // Every 5 items
713
+ return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1; // Every 10%
714
+ };
715
+
716
+ try {
717
+ for (let i = 0; i < operations.length; i++) {
718
+ const op = operations[i];
719
+ try {
720
+ // Replace variable references from previous operations
721
+ let processedData = JSON.stringify(op.data);
722
+ Object.keys(results.sys_ids).forEach(key => {
723
+ processedData = processedData.replace(`\${${key}}`, results.sys_ids[key]);
724
+ });
725
+ const data = JSON.parse(processedData);
726
+
727
+ const result = await this.createRecord(op.table, data);
728
+
729
+ // Save sys_id with the save_as key or operation index
730
+ const key = op.save_as || `operation_${i}`;
731
+ results.sys_ids[key] = result.sys_id;
732
+ results.created_count++;
733
+
734
+ // Report progress
735
+ if (shouldReport(i)) {
736
+ const percentage = Math.round(((i + 1) / total) * 100);
737
+ this.notifyProgress(`Creating record ${i + 1}/${total} (${percentage}%): ${op.table}`);
738
+ }
739
+ } catch (error) {
740
+ results.errors.push({
741
+ operation_index: i,
742
+ table: op.table,
743
+ error: error.message
744
+ });
745
+
746
+ if (reportProgress) {
747
+ this.notifyProgress(`Failed ${i + 1}/${total}: ${op.table} - ${error.message}`);
748
+ }
749
+
750
+ if (transaction) {
751
+ results.success = false;
752
+ throw new Error(`Batch create failed at operation ${i}: ${error.message}`);
753
+ }
754
+ }
755
+ }
756
+
757
+ // Final summary
758
+ if (reportProgress) {
759
+ const failedCount = results.errors.length;
760
+ if (failedCount > 0) {
761
+ this.notifyProgress(`Complete: ${results.created_count}/${total} records created (${failedCount} failed)`);
762
+ } else {
763
+ this.notifyProgress(`Complete: All ${total} records created successfully`);
764
+ }
765
+ }
766
+ } finally {
767
+ results.execution_time_ms = Date.now() - startTime;
768
+ }
769
+
770
+ return results;
771
+ }
772
+
773
+ async batchUpdate(updates, stopOnError = false, reportProgress = true) {
774
+ const results = {
775
+ success: true,
776
+ updated_count: 0,
777
+ errors: [],
778
+ execution_time_ms: 0
779
+ };
780
+
781
+ const startTime = Date.now();
782
+ const total = updates.length;
783
+
784
+ // Determine progress reporting frequency
785
+ const shouldReport = (index) => {
786
+ if (!reportProgress) return false;
787
+ if (total <= 10) return true; // Report every item for small batches
788
+ if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1; // Every 5 items
789
+ return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1; // Every 10%
790
+ };
791
+
792
+ for (let i = 0; i < updates.length; i++) {
793
+ const update = updates[i];
794
+ try {
795
+ await this.updateRecord(update.table, update.sys_id, update.data);
796
+ results.updated_count++;
797
+
798
+ // Report progress
799
+ if (shouldReport(i)) {
800
+ const percentage = Math.round(((i + 1) / total) * 100);
801
+ this.notifyProgress(`Updating record ${i + 1}/${total} (${percentage}%): ${update.table}`);
802
+ }
803
+ } catch (error) {
804
+ results.errors.push({
805
+ update_index: i,
806
+ table: update.table,
807
+ sys_id: update.sys_id,
808
+ error: error.message
809
+ });
810
+
811
+ if (reportProgress) {
812
+ this.notifyProgress(`Failed ${i + 1}/${total}: ${update.table} - ${error.message}`);
813
+ }
814
+
815
+ if (stopOnError) {
816
+ results.success = false;
817
+ break;
818
+ }
819
+ }
820
+ }
821
+
822
+ // Final summary
823
+ if (reportProgress) {
824
+ const failedCount = results.errors.length;
825
+ if (failedCount > 0) {
826
+ this.notifyProgress(`Complete: ${results.updated_count}/${total} records updated (${failedCount} failed)`);
827
+ } else {
828
+ this.notifyProgress(`Complete: All ${total} records updated successfully`);
829
+ }
830
+ }
831
+
832
+ results.execution_time_ms = Date.now() - startTime;
833
+ return results;
834
+ }
835
+
836
+ // Workflow creation methods
837
+ async createWorkflow(workflowData) {
838
+ // Create base workflow
839
+ const workflow = await this.createRecord('wf_workflow', {
840
+ name: workflowData.name,
841
+ description: workflowData.description || '',
842
+ template: workflowData.template || false,
843
+ access: workflowData.access || 'public'
844
+ });
845
+
846
+ return {
847
+ workflow_sys_id: workflow.sys_id,
848
+ name: workflowData.name
849
+ };
850
+ }
851
+
852
+ async createWorkflowVersion(versionData) {
853
+ // Create workflow version
854
+ const version = await this.createRecord('wf_workflow_version', {
855
+ name: versionData.name,
856
+ workflow: versionData.workflow_sys_id,
857
+ table: versionData.table,
858
+ description: versionData.description || '',
859
+ active: versionData.active !== undefined ? versionData.active : true,
860
+ published: versionData.published || false,
861
+ condition: versionData.condition || '',
862
+ order: versionData.order || 100,
863
+ run_multiple: versionData.run_multiple || false,
864
+ after_business_rules: versionData.after_business_rules !== undefined ? versionData.after_business_rules : true,
865
+ expected_time: versionData.expected_time || '',
866
+ condition_type: versionData.condition_type || ''
867
+ });
868
+
869
+ return {
870
+ version_sys_id: version.sys_id,
871
+ name: versionData.name
872
+ };
873
+ }
874
+
875
+ async createActivity(activityData) {
876
+ // Create activity
877
+ const activity = await this.createRecord('wf_activity', {
878
+ name: activityData.name,
879
+ workflow_version: activityData.workflow_version_sys_id,
880
+ activity_definition: activityData.activity_definition_sys_id || '',
881
+ x: activityData.x || 100,
882
+ y: activityData.y || 100,
883
+ width: activityData.width || 150,
884
+ height: activityData.height || 80,
885
+ input: activityData.script || activityData.input || '',
886
+ vars: activityData.vars || '',
887
+ stage: activityData.stage_sys_id || '',
888
+ parent: activityData.parent_sys_id || '',
889
+ timeout: activityData.timeout || '0 00:00:00'
890
+ });
891
+
892
+ return {
893
+ activity_sys_id: activity.sys_id,
894
+ name: activityData.name
895
+ };
896
+ }
897
+
898
+ async createCondition(conditionData) {
899
+ // Create condition
900
+ const condition = await this.createRecord('wf_condition', {
901
+ activity: conditionData.activity_sys_id,
902
+ name: conditionData.name,
903
+ short_description: conditionData.description || '',
904
+ condition: conditionData.condition || '',
905
+ order: conditionData.order || 1,
906
+ else_flag: conditionData.else_flag || false,
907
+ event: conditionData.event || false,
908
+ event_name: conditionData.event_name || '',
909
+ condition_type: conditionData.condition_type || 'standard'
910
+ });
911
+
912
+ return {
913
+ condition_sys_id: condition.sys_id,
914
+ name: conditionData.name
915
+ };
916
+ }
917
+
918
+ async createTransition(transitionData) {
919
+ // Create transition
920
+ const transition = await this.createRecord('wf_transition', {
921
+ from: transitionData.from_activity_sys_id,
922
+ to: transitionData.to_activity_sys_id,
923
+ condition: transitionData.condition_sys_id || '',
924
+ order: transitionData.order || 1
925
+ });
926
+
927
+ return {
928
+ transition_sys_id: transition.sys_id
929
+ };
930
+ }
931
+
932
+ async publishWorkflow(versionSysId, startActivitySysId) {
933
+ // Update workflow version to set start activity and publish
934
+ const updated = await this.updateRecord('wf_workflow_version', versionSysId, {
935
+ start: startActivitySysId,
936
+ published: true
937
+ });
938
+
939
+ return {
940
+ version_sys_id: versionSysId,
941
+ published: true,
942
+ start_activity: startActivitySysId
943
+ };
944
+ }
945
+
946
+ async createCompleteWorkflow(workflowSpec, reportProgress = true) {
947
+ // Create complete workflow with activities and transitions in one call
948
+ const results = {
949
+ workflow_sys_id: '',
950
+ version_sys_id: '',
951
+ activity_sys_ids: {},
952
+ transition_sys_ids: [],
953
+ published: false
954
+ };
955
+
956
+ try {
957
+ // 1. Create base workflow
958
+ if (reportProgress) this.notifyProgress('Creating workflow base');
959
+ const workflow = await this.createWorkflow({
960
+ name: workflowSpec.name,
961
+ description: workflowSpec.description,
962
+ template: workflowSpec.template,
963
+ access: workflowSpec.access
964
+ });
965
+ results.workflow_sys_id = workflow.workflow_sys_id;
966
+
967
+ // 2. Create workflow version
968
+ if (reportProgress) this.notifyProgress('Creating workflow version');
969
+ const version = await this.createWorkflowVersion({
970
+ name: workflowSpec.name,
971
+ workflow_sys_id: workflow.workflow_sys_id,
972
+ table: workflowSpec.table,
973
+ description: workflowSpec.description,
974
+ active: workflowSpec.active,
975
+ published: false, // Don't publish yet
976
+ condition: workflowSpec.condition,
977
+ order: workflowSpec.order,
978
+ run_multiple: workflowSpec.run_multiple,
979
+ after_business_rules: workflowSpec.after_business_rules
980
+ });
981
+ results.version_sys_id = version.version_sys_id;
982
+
983
+ // 3. Create activities
984
+ const activities = workflowSpec.activities || [];
985
+ const totalActivities = activities.length;
986
+ for (let i = 0; i < activities.length; i++) {
987
+ const actSpec = activities[i];
988
+
989
+ if (reportProgress) {
990
+ this.notifyProgress(`Creating activity ${i + 1}/${totalActivities}: ${actSpec.name}`);
991
+ }
992
+
993
+ const activity = await this.createActivity({
994
+ name: actSpec.name,
995
+ workflow_version_sys_id: version.version_sys_id,
996
+ activity_definition_sys_id: actSpec.activity_type,
997
+ x: actSpec.x !== undefined ? actSpec.x : (100 + i * 150),
998
+ y: actSpec.y !== undefined ? actSpec.y : 100,
999
+ width: actSpec.width,
1000
+ height: actSpec.height,
1001
+ script: actSpec.script,
1002
+ vars: actSpec.vars,
1003
+ stage_sys_id: actSpec.stage,
1004
+ parent_sys_id: actSpec.parent,
1005
+ timeout: actSpec.timeout
1006
+ });
1007
+
1008
+ const key = actSpec.id || `activity_${i}`;
1009
+ results.activity_sys_ids[key] = activity.activity_sys_id;
1010
+ }
1011
+
1012
+ // 4. Create transitions
1013
+ const transitions = workflowSpec.transitions || [];
1014
+ const totalTransitions = transitions.length;
1015
+ for (let i = 0; i < transitions.length; i++) {
1016
+ const transSpec = transitions[i];
1017
+
1018
+ if (reportProgress) {
1019
+ this.notifyProgress(`Creating transition ${i + 1}/${totalTransitions}`);
1020
+ }
1021
+
1022
+ // Resolve activity references
1023
+ const fromId = typeof transSpec.from === 'number'
1024
+ ? results.activity_sys_ids[`activity_${transSpec.from}`]
1025
+ : results.activity_sys_ids[transSpec.from] || transSpec.from;
1026
+
1027
+ const toId = typeof transSpec.to === 'number'
1028
+ ? results.activity_sys_ids[`activity_${transSpec.to}`]
1029
+ : results.activity_sys_ids[transSpec.to] || transSpec.to;
1030
+
1031
+ // Create condition if specified
1032
+ let conditionId = transSpec.condition_sys_id;
1033
+ if (transSpec.condition && !conditionId) {
1034
+ const condition = await this.createCondition({
1035
+ activity_sys_id: fromId,
1036
+ name: transSpec.condition_name || 'Condition',
1037
+ description: transSpec.condition_description,
1038
+ condition: transSpec.condition,
1039
+ order: transSpec.order,
1040
+ else_flag: transSpec.else_flag
1041
+ });
1042
+ conditionId = condition.condition_sys_id;
1043
+ }
1044
+
1045
+ // Create transition
1046
+ const transition = await this.createTransition({
1047
+ from_activity_sys_id: fromId,
1048
+ to_activity_sys_id: toId,
1049
+ condition_sys_id: conditionId,
1050
+ order: transSpec.order
1051
+ });
1052
+
1053
+ results.transition_sys_ids.push(transition.transition_sys_id);
1054
+ }
1055
+
1056
+ // 5. Publish if requested
1057
+ if (workflowSpec.publish && activities.length > 0) {
1058
+ if (reportProgress) this.notifyProgress('Publishing workflow');
1059
+
1060
+ const startActivityId = workflowSpec.start_activity
1061
+ ? (results.activity_sys_ids[workflowSpec.start_activity] || workflowSpec.start_activity)
1062
+ : results.activity_sys_ids['activity_0'] || results.activity_sys_ids[Object.keys(results.activity_sys_ids)[0]];
1063
+
1064
+ await this.publishWorkflow(version.version_sys_id, startActivityId);
1065
+ results.published = true;
1066
+ results.start_activity = startActivityId;
1067
+ }
1068
+
1069
+ if (reportProgress) {
1070
+ this.notifyProgress(`Complete: Workflow created with ${totalActivities} activities and ${totalTransitions} transitions`);
1071
+ }
1072
+
1073
+ return results;
1074
+ } catch (error) {
1075
+ throw new Error(`Failed to create workflow: ${error.message}`);
1076
+ }
1077
+ }
1078
+
1079
+ // Move records to update set
1080
+ async moveRecordsToUpdateSet(updateSetId, options = {}) {
1081
+ const {
1082
+ record_sys_ids = [],
1083
+ time_range = null,
1084
+ source_update_set = null,
1085
+ table = 'sys_update_xml',
1086
+ reportProgress = true
1087
+ } = options;
1088
+
1089
+ try {
1090
+ const results = {
1091
+ moved: 0,
1092
+ failed: 0,
1093
+ records: [],
1094
+ errors: []
1095
+ };
1096
+
1097
+ let recordsToMove = [];
1098
+
1099
+ // Get records by sys_ids
1100
+ if (record_sys_ids.length > 0) {
1101
+ if (reportProgress) this.notifyProgress(`Fetching ${record_sys_ids.length} records to move`);
1102
+ const sysIdsQuery = record_sys_ids.map(id => `sys_id=${id}`).join('^OR');
1103
+ recordsToMove = await this.getRecords(table, {
1104
+ sysparm_query: sysIdsQuery,
1105
+ sysparm_limit: 1000
1106
+ });
1107
+ }
1108
+ // Get records by time range
1109
+ else if (time_range) {
1110
+ if (reportProgress) this.notifyProgress('Fetching records by time range');
1111
+ let query = `sys_created_on>=${time_range.start}^sys_created_on<=${time_range.end}`;
1112
+ if (source_update_set) {
1113
+ query += `^update_set.name=${source_update_set}`;
1114
+ }
1115
+ recordsToMove = await this.getRecords(table, {
1116
+ sysparm_query: query,
1117
+ sysparm_limit: 1000
1118
+ });
1119
+ }
1120
+
1121
+ const total = recordsToMove.length;
1122
+ if (total === 0) {
1123
+ if (reportProgress) this.notifyProgress('No records found to move');
1124
+ return results;
1125
+ }
1126
+
1127
+ if (reportProgress) this.notifyProgress(`Moving ${total} records to update set`);
1128
+
1129
+ // Determine progress reporting frequency
1130
+ const shouldReport = (index) => {
1131
+ if (!reportProgress) return false;
1132
+ if (total <= 10) return true;
1133
+ if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1;
1134
+ return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1;
1135
+ };
1136
+
1137
+ // Move each record
1138
+ for (let i = 0; i < recordsToMove.length; i++) {
1139
+ const record = recordsToMove[i];
1140
+ try {
1141
+ await this.updateRecord(table, record.sys_id, {
1142
+ update_set: updateSetId
1143
+ });
1144
+ results.moved++;
1145
+ results.records.push({
1146
+ sys_id: record.sys_id,
1147
+ name: record.name,
1148
+ type: record.type,
1149
+ status: 'moved'
1150
+ });
1151
+
1152
+ if (shouldReport(i)) {
1153
+ const percentage = Math.round(((i + 1) / total) * 100);
1154
+ this.notifyProgress(`Moving record ${i + 1}/${total} (${percentage}%): ${record.type || 'unknown'}`);
1155
+ }
1156
+ } catch (error) {
1157
+ results.failed++;
1158
+ results.errors.push({
1159
+ sys_id: record.sys_id,
1160
+ error: error.message
1161
+ });
1162
+
1163
+ if (reportProgress) {
1164
+ this.notifyProgress(`Failed ${i + 1}/${total}: ${record.sys_id} - ${error.message}`);
1165
+ }
1166
+ }
1167
+ }
1168
+
1169
+ if (reportProgress) {
1170
+ if (results.failed > 0) {
1171
+ this.notifyProgress(`Complete: ${results.moved}/${total} records moved (${results.failed} failed)`);
1172
+ } else {
1173
+ this.notifyProgress(`Complete: All ${total} records moved successfully`);
1174
+ }
1175
+ }
1176
+
1177
+ return results;
1178
+ } catch (error) {
1179
+ throw new Error(`Failed to move records to update set: ${error.message}`);
1180
+ }
1181
+ }
1182
+
1183
+ // Clone update set
1184
+ async cloneUpdateSet(sourceUpdateSetId, newName, reportProgress = true) {
1185
+ try {
1186
+ // Get source update set
1187
+ if (reportProgress) this.notifyProgress('Fetching source update set');
1188
+ const sourceSet = await this.getRecord('sys_update_set', sourceUpdateSetId);
1189
+
1190
+ // Create new update set
1191
+ if (reportProgress) this.notifyProgress(`Creating new update set: ${newName}`);
1192
+ const newSet = await this.createRecord('sys_update_set', {
1193
+ name: newName,
1194
+ description: `Clone of: ${sourceSet.name}\n\n${sourceSet.description || ''}`,
1195
+ application: sourceSet.application,
1196
+ state: 'in progress'
1197
+ });
1198
+
1199
+ // Get all update XML records from source
1200
+ if (reportProgress) this.notifyProgress('Fetching update records from source');
1201
+ const updateRecords = await this.getRecords('sys_update_xml', {
1202
+ sysparm_query: `update_set=${sourceUpdateSetId}`,
1203
+ sysparm_limit: 5000
1204
+ });
1205
+
1206
+ const total = updateRecords.length;
1207
+ if (reportProgress) this.notifyProgress(`Cloning ${total} update records`);
1208
+
1209
+ // Determine progress reporting frequency
1210
+ const shouldReport = (index) => {
1211
+ if (!reportProgress) return false;
1212
+ if (total <= 10) return true;
1213
+ if (total <= 50) return (index + 1) % 5 === 0 || index === total - 1;
1214
+ return (index + 1) % Math.ceil(total / 10) === 0 || index === total - 1;
1215
+ };
1216
+
1217
+ // Clone each update record (create new records pointing to new set)
1218
+ const clonedRecords = [];
1219
+ let failedCount = 0;
1220
+ for (let i = 0; i < updateRecords.length; i++) {
1221
+ const record = updateRecords[i];
1222
+ try {
1223
+ const cloned = await this.createRecord('sys_update_xml', {
1224
+ update_set: newSet.sys_id,
1225
+ name: record.name,
1226
+ type: record.type,
1227
+ target_name: record.target_name,
1228
+ payload: record.payload,
1229
+ category: record.category
1230
+ });
1231
+ clonedRecords.push(cloned);
1232
+
1233
+ if (shouldReport(i)) {
1234
+ const percentage = Math.round(((i + 1) / total) * 100);
1235
+ this.notifyProgress(`Cloning record ${i + 1}/${total} (${percentage}%): ${record.type || 'unknown'}`);
1236
+ }
1237
+ } catch (error) {
1238
+ failedCount++;
1239
+ console.error(`Failed to clone record ${record.sys_id}: ${error.message}`);
1240
+ if (reportProgress && failedCount <= 5) { // Only report first 5 failures to avoid spam
1241
+ this.notifyProgress(`Failed to clone record ${i + 1}/${total}: ${error.message}`);
1242
+ }
1243
+ }
1244
+ }
1245
+
1246
+ if (reportProgress) {
1247
+ if (failedCount > 0) {
1248
+ this.notifyProgress(`Complete: ${clonedRecords.length}/${total} records cloned (${failedCount} failed)`);
1249
+ } else {
1250
+ this.notifyProgress(`Complete: All ${total} records cloned successfully`);
1251
+ }
1252
+ }
1253
+
1254
+ return {
1255
+ new_update_set_id: newSet.sys_id,
1256
+ new_update_set_name: newSet.name,
1257
+ source_update_set_id: sourceUpdateSetId,
1258
+ source_update_set_name: sourceSet.name,
1259
+ records_cloned: clonedRecords.length,
1260
+ total_source_records: updateRecords.length
1261
+ };
1262
+ } catch (error) {
1263
+ throw new Error(`Failed to clone update set: ${error.message}`);
1264
+ }
1265
+ }
1266
+
1267
+ // Enhanced schema discovery
1268
+ async discoverTableSchema(tableName, options = {}) {
1269
+ const {
1270
+ include_type_codes = false,
1271
+ include_choice_tables = false,
1272
+ include_relationships = false,
1273
+ include_ui_policies = false,
1274
+ include_business_rules = false,
1275
+ include_field_constraints = false
1276
+ } = options;
1277
+
1278
+ const schema = {
1279
+ table: tableName,
1280
+ label: null,
1281
+ fields: []
1282
+ };
1283
+
1284
+ try {
1285
+ // Get table metadata
1286
+ const tables = await this.getRecords('sys_db_object', {
1287
+ sysparm_query: `name=${tableName}`,
1288
+ sysparm_limit: 1
1289
+ });
1290
+
1291
+ if (tables.length > 0) {
1292
+ schema.label = tables[0].label;
1293
+ schema.super_class = tables[0].super_class?.value;
1294
+ }
1295
+
1296
+ // Get field definitions
1297
+ const fields = await this.getRecords('sys_dictionary', {
1298
+ sysparm_query: `name=${tableName}`,
1299
+ sysparm_limit: 1000
1300
+ });
1301
+
1302
+ for (const field of fields) {
1303
+ const fieldInfo = {
1304
+ name: field.element,
1305
+ label: field.column_label,
1306
+ internal_type: field.internal_type?.value || field.internal_type,
1307
+ max_length: field.max_length,
1308
+ mandatory: field.mandatory === 'true' || field.mandatory === true,
1309
+ read_only: field.read_only === 'true' || field.read_only === true
1310
+ };
1311
+
1312
+ // Add type codes for integer fields (like variable types)
1313
+ if (include_type_codes && fieldInfo.internal_type === 'integer' && field.element === 'type') {
1314
+ const choices = await this.getRecords('sys_choice', {
1315
+ sysparm_query: `name=${tableName}^element=type`,
1316
+ sysparm_limit: 100
1317
+ });
1318
+
1319
+ if (choices.length > 0) {
1320
+ fieldInfo.type_codes = {};
1321
+ choices.forEach(choice => {
1322
+ fieldInfo.type_codes[choice.value] = choice.label;
1323
+ });
1324
+ }
1325
+ }
1326
+
1327
+ // Add reference information
1328
+ if (fieldInfo.internal_type === 'reference' && field.reference) {
1329
+ fieldInfo.reference_table = field.reference.value || field.reference;
1330
+ }
1331
+
1332
+ if (include_field_constraints && field.default_value) {
1333
+ fieldInfo.default_value = field.default_value;
1334
+ }
1335
+
1336
+ schema.fields.push(fieldInfo);
1337
+ }
1338
+
1339
+ // Choice tables
1340
+ if (include_choice_tables) {
1341
+ schema.choice_tables = {
1342
+ sys_choice: 'For table field choices',
1343
+ question_choice: 'For catalog variable choices'
1344
+ };
1345
+ }
1346
+
1347
+ // Relationships
1348
+ if (include_relationships) {
1349
+ schema.relationships = {};
1350
+ const refFields = schema.fields.filter(f => f.internal_type === 'reference');
1351
+ for (const field of refFields) {
1352
+ if (field.reference_table) {
1353
+ schema.relationships[field.name] = {
1354
+ type: 'reference',
1355
+ table: field.reference_table,
1356
+ description: field.label
1357
+ };
1358
+ }
1359
+ }
1360
+ }
1361
+
1362
+ // UI Policies
1363
+ if (include_ui_policies) {
1364
+ const policies = await this.getRecords('sys_ui_policy', {
1365
+ sysparm_query: `table=${tableName}^active=true`,
1366
+ sysparm_fields: 'sys_id,short_description',
1367
+ sysparm_limit: 100
1368
+ });
1369
+ schema.ui_policies = policies.map(p => ({
1370
+ sys_id: p.sys_id,
1371
+ description: p.short_description
1372
+ }));
1373
+ }
1374
+
1375
+ // Business Rules
1376
+ if (include_business_rules) {
1377
+ const rules = await this.getRecords('sys_script', {
1378
+ sysparm_query: `collection=${tableName}^active=true`,
1379
+ sysparm_fields: 'sys_id,name,when',
1380
+ sysparm_limit: 100
1381
+ });
1382
+ schema.business_rules = rules.map(r => ({
1383
+ sys_id: r.sys_id,
1384
+ name: r.name,
1385
+ when: r.when
1386
+ }));
1387
+ }
1388
+
1389
+ } catch (error) {
1390
+ throw new Error(`Failed to discover schema for ${tableName}: ${error.message}`);
1391
+ }
1392
+
1393
+ return schema;
1394
+ }
1395
+
1396
+ // Field explanation
1397
+ async explainField(tableName, fieldName, includeExamples = true) {
1398
+ try {
1399
+ const fields = await this.getRecords('sys_dictionary', {
1400
+ sysparm_query: `name=${tableName}^element=${fieldName}`,
1401
+ sysparm_limit: 1
1402
+ });
1403
+
1404
+ if (fields.length === 0) {
1405
+ throw new Error(`Field ${fieldName} not found in table ${tableName}`);
1406
+ }
1407
+
1408
+ const field = fields[0];
1409
+ const explanation = {
1410
+ field: fieldName,
1411
+ table: tableName,
1412
+ label: field.column_label,
1413
+ type: field.internal_type?.value || field.internal_type,
1414
+ max_length: field.max_length,
1415
+ mandatory: field.mandatory === 'true' || field.mandatory === true,
1416
+ read_only: field.read_only === 'true' || field.read_only === true,
1417
+ comments: field.comments,
1418
+ help: field.help
1419
+ };
1420
+
1421
+ // Get reference info
1422
+ if (field.reference) {
1423
+ explanation.reference_table = field.reference.value || field.reference;
1424
+ }
1425
+
1426
+ // Get choices for choice fields
1427
+ if (field.internal_type === 'choice' || field.internal_type === 'integer') {
1428
+ const choices = await this.getRecords('sys_choice', {
1429
+ sysparm_query: `name=${tableName}^element=${fieldName}`,
1430
+ sysparm_limit: 100
1431
+ });
1432
+
1433
+ if (choices.length > 0) {
1434
+ explanation.choices = choices.map(c => ({
1435
+ value: c.value,
1436
+ label: c.label
1437
+ }));
1438
+ }
1439
+ }
1440
+
1441
+ // Known issues for specific fields
1442
+ if (tableName === 'catalog_ui_policy_action' && (fieldName === 'ui_policy' || fieldName === 'catalog_variable')) {
1443
+ explanation.known_issues = [
1444
+ 'Cannot be set via REST API - use background script with setValue()',
1445
+ fieldName === 'catalog_variable' ? 'Must include IO: prefix or linkage will fail' : null
1446
+ ].filter(Boolean);
1447
+
1448
+ if (fieldName === 'catalog_variable') {
1449
+ explanation.special_format = 'IO:<variable_sys_id>';
1450
+ if (includeExamples) {
1451
+ explanation.example = 'IO:94ababd1c35432101fcbbd43e40131bf';
1452
+ }
1453
+ }
1454
+ }
1455
+
1456
+ return explanation;
1457
+ } catch (error) {
1458
+ throw new Error(`Failed to explain field: ${error.message}`);
1459
+ }
1460
+ }
1461
+
1462
+ // Validate catalog configuration
1463
+ async validateCatalogConfiguration(catalogItemSysId, checks = {}) {
1464
+ const results = {
1465
+ valid: true,
1466
+ issues: [],
1467
+ warnings: 0,
1468
+ errors: 0
1469
+ };
1470
+
1471
+ try {
1472
+ // Validate variables
1473
+ if (checks.variables) {
1474
+ const variables = await this.getRecords('item_option_new', {
1475
+ sysparm_query: `cat_item=${catalogItemSysId}`,
1476
+ sysparm_limit: 1000
1477
+ });
1478
+
1479
+ for (const variable of variables) {
1480
+ // Check if linked
1481
+ if (checks.variables.check_linked && !variable.cat_item) {
1482
+ results.issues.push({
1483
+ severity: 'error',
1484
+ component: 'variable',
1485
+ sys_id: variable.sys_id,
1486
+ issue: `Variable ${variable.name} is not linked to catalog item`,
1487
+ fix: 'Update cat_item field'
1488
+ });
1489
+ results.errors++;
1490
+ results.valid = false;
1491
+ }
1492
+
1493
+ // Check tooltip length
1494
+ if (variable.tooltip && variable.tooltip.length > 40) {
1495
+ results.issues.push({
1496
+ severity: 'warning',
1497
+ component: 'variable',
1498
+ sys_id: variable.sys_id,
1499
+ issue: `Tooltip exceeds 40 characters and will be truncated (${variable.tooltip.length} chars)`,
1500
+ fix: 'Move detailed help to help_text field'
1501
+ });
1502
+ results.warnings++;
1503
+ }
1504
+
1505
+ // Check for choices
1506
+ if (checks.variables.check_choices && (variable.type === '1' || variable.type === '5')) {
1507
+ const choices = await this.getRecords('question_choice', {
1508
+ sysparm_query: `question=${variable.sys_id}`,
1509
+ sysparm_limit: 1
1510
+ });
1511
+
1512
+ if (choices.length === 0) {
1513
+ results.issues.push({
1514
+ severity: 'error',
1515
+ component: 'variable',
1516
+ sys_id: variable.sys_id,
1517
+ issue: `Variable ${variable.name} is type ${variable.type === '1' ? 'Choice' : 'Select Box'} but has no choices defined`,
1518
+ fix: 'Add choices via question_choice table'
1519
+ });
1520
+ results.errors++;
1521
+ results.valid = false;
1522
+ }
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ // Validate UI policies
1528
+ if (checks.ui_policies) {
1529
+ const policies = await this.getRecords('catalog_ui_policy', {
1530
+ sysparm_query: `catalog_item=${catalogItemSysId}`,
1531
+ sysparm_limit: 1000
1532
+ });
1533
+
1534
+ for (const policy of policies) {
1535
+ if (checks.ui_policies.check_actions_linked) {
1536
+ const actions = await this.getRecords('catalog_ui_policy_action', {
1537
+ sysparm_query: `ui_policy=${policy.sys_id}`,
1538
+ sysparm_limit: 1000
1539
+ });
1540
+
1541
+ for (const action of actions) {
1542
+ if (!action.catalog_variable || action.catalog_variable === '') {
1543
+ results.issues.push({
1544
+ severity: 'error',
1545
+ component: 'ui_policy_action',
1546
+ sys_id: action.sys_id,
1547
+ issue: 'catalog_variable field is empty - action not linked to policy',
1548
+ fix: 'Run background script to set catalog_variable value'
1549
+ });
1550
+ results.errors++;
1551
+ results.valid = false;
1552
+ }
1553
+ }
1554
+ }
1555
+ }
1556
+ }
1557
+
1558
+ } catch (error) {
1559
+ results.issues.push({
1560
+ severity: 'error',
1561
+ component: 'validation',
1562
+ issue: `Validation failed: ${error.message}`
1563
+ });
1564
+ results.errors++;
1565
+ results.valid = false;
1566
+ }
1567
+
1568
+ return results;
1569
+ }
1570
+
1571
+ // Inspect update set
1572
+ async inspectUpdateSet(updateSetSysId, options = {}) {
1573
+ const {
1574
+ show_components = true,
1575
+ show_dependencies = false
1576
+ } = options;
1577
+
1578
+ try {
1579
+ const updateSet = await this.getRecord('sys_update_set', updateSetSysId);
1580
+
1581
+ const result = {
1582
+ update_set: {
1583
+ sys_id: updateSet.sys_id,
1584
+ name: updateSet.name,
1585
+ state: updateSet.state,
1586
+ description: updateSet.description
1587
+ },
1588
+ total_records: 0,
1589
+ components: []
1590
+ };
1591
+
1592
+ if (show_components) {
1593
+ const updates = await this.getRecords('sys_update_xml', {
1594
+ sysparm_query: `update_set=${updateSetSysId}`,
1595
+ sysparm_fields: 'type,name,target_name',
1596
+ sysparm_limit: 10000
1597
+ });
1598
+
1599
+ result.total_records = updates.length;
1600
+
1601
+ // Group by type
1602
+ const typeGroups = {};
1603
+ updates.forEach(update => {
1604
+ const type = update.type || 'unknown';
1605
+ if (!typeGroups[type]) {
1606
+ typeGroups[type] = [];
1607
+ }
1608
+ typeGroups[type].push(update);
1609
+ });
1610
+
1611
+ result.components = Object.keys(typeGroups).map(type => ({
1612
+ type,
1613
+ count: typeGroups[type].length,
1614
+ items: typeGroups[type].slice(0, 10).map(u => u.name || u.target_name)
1615
+ }));
1616
+ }
1617
+
1618
+ result.ready_to_deploy = result.update_set.state === 'complete';
1619
+
1620
+ return result;
1621
+ } catch (error) {
1622
+ throw new Error(`Failed to inspect update set: ${error.message}`);
1623
+ }
1624
+ }
1625
+ }