snow-flow 3.3.7 → 3.3.8

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.
@@ -131,6 +131,18 @@
131
131
  "SNOW_CLIENT_ID": "{{SNOW_CLIENT_ID}}",
132
132
  "SNOW_CLIENT_SECRET": "{{SNOW_CLIENT_SECRET}}"
133
133
  }
134
+ },
135
+ "servicenow-system-properties": {
136
+ "command": "node",
137
+ "args": [
138
+ "{{PROJECT_ROOT}}/dist/mcp/servicenow-system-properties-mcp.js"
139
+ ],
140
+ "description": "System property management via official ServiceNow APIs: get/set/list/delete properties, bulk operations, import/export JSON, validate values, search, categories, audit history - all using standard Table API on sys_properties",
141
+ "env": {
142
+ "SNOW_INSTANCE": "{{SNOW_INSTANCE}}",
143
+ "SNOW_CLIENT_ID": "{{SNOW_CLIENT_ID}}",
144
+ "SNOW_CLIENT_SECRET": "{{SNOW_CLIENT_SECRET}}"
145
+ }
134
146
  }
135
147
  }
136
148
  }
@@ -36,7 +36,7 @@ function getDynamicVersion() {
36
36
  console.warn('Warning: Could not read version from package.json:', error);
37
37
  }
38
38
  // Fallback to hardcoded version
39
- return '3.3.7';
39
+ return '3.3.8';
40
40
  }
41
41
  // Export a constant that uses the dynamic version
42
42
  exports.VERSION = getDynamicVersion();
@@ -0,0 +1,68 @@
1
+ /**
2
+ * ServiceNow System Properties MCP Server
3
+ *
4
+ * Provides comprehensive system property management through official ServiceNow APIs
5
+ * Uses the standard Table API on sys_properties table
6
+ */
7
+ /**
8
+ * ServiceNow System Properties MCP Server
9
+ * Manages system properties through official ServiceNow REST APIs
10
+ */
11
+ export declare class ServiceNowSystemPropertiesMCP {
12
+ private server;
13
+ private client;
14
+ private propertyCache;
15
+ constructor();
16
+ private setupHandlers;
17
+ private setupTools;
18
+ /**
19
+ * Get a system property value
20
+ */
21
+ private getProperty;
22
+ /**
23
+ * Set or create a system property
24
+ */
25
+ private setProperty;
26
+ /**
27
+ * List system properties
28
+ */
29
+ private listProperties;
30
+ /**
31
+ * Delete a system property
32
+ */
33
+ private deleteProperty;
34
+ /**
35
+ * Search properties
36
+ */
37
+ private searchProperties;
38
+ /**
39
+ * Bulk get properties
40
+ */
41
+ private bulkGetProperties;
42
+ /**
43
+ * Bulk set properties
44
+ */
45
+ private bulkSetProperties;
46
+ /**
47
+ * Export properties
48
+ */
49
+ private exportProperties;
50
+ /**
51
+ * Import properties
52
+ */
53
+ private importProperties;
54
+ /**
55
+ * Validate property value
56
+ */
57
+ private validateProperty;
58
+ /**
59
+ * Get property categories
60
+ */
61
+ private getCategories;
62
+ /**
63
+ * Get property audit history
64
+ */
65
+ private getPropertyHistory;
66
+ start(): Promise<void>;
67
+ }
68
+ //# sourceMappingURL=servicenow-system-properties-mcp.d.ts.map
@@ -0,0 +1,1142 @@
1
+ "use strict";
2
+ /**
3
+ * ServiceNow System Properties MCP Server
4
+ *
5
+ * Provides comprehensive system property management through official ServiceNow APIs
6
+ * Uses the standard Table API on sys_properties table
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ServiceNowSystemPropertiesMCP = void 0;
10
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
11
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
12
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
13
+ const servicenow_client_js_1 = require("../utils/servicenow-client.js");
14
+ const logger_js_1 = require("../utils/logger.js");
15
+ const logger = new logger_js_1.Logger('ServiceNowSystemProperties');
16
+ /**
17
+ * ServiceNow System Properties MCP Server
18
+ * Manages system properties through official ServiceNow REST APIs
19
+ */
20
+ class ServiceNowSystemPropertiesMCP {
21
+ constructor() {
22
+ this.propertyCache = new Map();
23
+ this.server = new index_js_1.Server({
24
+ name: 'servicenow-system-properties',
25
+ version: '1.0.0',
26
+ }, {
27
+ capabilities: {
28
+ tools: {},
29
+ },
30
+ });
31
+ this.client = new servicenow_client_js_1.ServiceNowClient();
32
+ this.setupHandlers();
33
+ this.setupTools();
34
+ }
35
+ setupHandlers() {
36
+ this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
37
+ tools: [
38
+ {
39
+ name: 'snow_property_get',
40
+ description: 'Get a system property value by name',
41
+ inputSchema: {
42
+ type: 'object',
43
+ properties: {
44
+ name: {
45
+ type: 'string',
46
+ description: 'Property name (e.g., glide.servlet.uri)'
47
+ },
48
+ include_metadata: {
49
+ type: 'boolean',
50
+ description: 'Include full property metadata',
51
+ default: false
52
+ }
53
+ },
54
+ required: ['name']
55
+ }
56
+ },
57
+ {
58
+ name: 'snow_property_set',
59
+ description: 'Set or update a system property value',
60
+ inputSchema: {
61
+ type: 'object',
62
+ properties: {
63
+ name: {
64
+ type: 'string',
65
+ description: 'Property name'
66
+ },
67
+ value: {
68
+ type: 'string',
69
+ description: 'Property value'
70
+ },
71
+ description: {
72
+ type: 'string',
73
+ description: 'Property description (optional)'
74
+ },
75
+ type: {
76
+ type: 'string',
77
+ description: 'Property type (string, boolean, integer, etc.)',
78
+ default: 'string'
79
+ },
80
+ choices: {
81
+ type: 'string',
82
+ description: 'Comma-separated list of valid choices (optional)'
83
+ },
84
+ is_private: {
85
+ type: 'boolean',
86
+ description: 'Mark property as private',
87
+ default: false
88
+ },
89
+ suffix: {
90
+ type: 'string',
91
+ description: 'Property suffix/scope (optional)'
92
+ }
93
+ },
94
+ required: ['name', 'value']
95
+ }
96
+ },
97
+ {
98
+ name: 'snow_property_list',
99
+ description: 'List system properties with optional filtering',
100
+ inputSchema: {
101
+ type: 'object',
102
+ properties: {
103
+ pattern: {
104
+ type: 'string',
105
+ description: 'Name pattern to filter (e.g., glide.* for all glide properties)'
106
+ },
107
+ category: {
108
+ type: 'string',
109
+ description: 'Property category filter'
110
+ },
111
+ is_private: {
112
+ type: 'boolean',
113
+ description: 'Filter by private properties'
114
+ },
115
+ limit: {
116
+ type: 'number',
117
+ description: 'Maximum number of properties to return',
118
+ default: 100
119
+ },
120
+ include_values: {
121
+ type: 'boolean',
122
+ description: 'Include property values in response',
123
+ default: true
124
+ }
125
+ }
126
+ }
127
+ },
128
+ {
129
+ name: 'snow_property_delete',
130
+ description: 'Delete a system property',
131
+ inputSchema: {
132
+ type: 'object',
133
+ properties: {
134
+ name: {
135
+ type: 'string',
136
+ description: 'Property name to delete'
137
+ },
138
+ confirm: {
139
+ type: 'boolean',
140
+ description: 'Confirmation flag (must be true)',
141
+ default: false
142
+ }
143
+ },
144
+ required: ['name', 'confirm']
145
+ }
146
+ },
147
+ {
148
+ name: 'snow_property_search',
149
+ description: 'Search properties by name or value content',
150
+ inputSchema: {
151
+ type: 'object',
152
+ properties: {
153
+ search_term: {
154
+ type: 'string',
155
+ description: 'Search term to find in property names or values'
156
+ },
157
+ search_in: {
158
+ type: 'string',
159
+ description: 'Where to search: name, value, description, or all',
160
+ default: 'all'
161
+ },
162
+ limit: {
163
+ type: 'number',
164
+ description: 'Maximum results',
165
+ default: 50
166
+ }
167
+ },
168
+ required: ['search_term']
169
+ }
170
+ },
171
+ {
172
+ name: 'snow_property_bulk_get',
173
+ description: 'Get multiple properties at once',
174
+ inputSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ names: {
178
+ type: 'array',
179
+ items: { type: 'string' },
180
+ description: 'Array of property names to retrieve'
181
+ },
182
+ include_metadata: {
183
+ type: 'boolean',
184
+ description: 'Include full metadata for each property',
185
+ default: false
186
+ }
187
+ },
188
+ required: ['names']
189
+ }
190
+ },
191
+ {
192
+ name: 'snow_property_bulk_set',
193
+ description: 'Set multiple properties at once',
194
+ inputSchema: {
195
+ type: 'object',
196
+ properties: {
197
+ properties: {
198
+ type: 'array',
199
+ items: {
200
+ type: 'object',
201
+ properties: {
202
+ name: { type: 'string' },
203
+ value: { type: 'string' },
204
+ description: { type: 'string' },
205
+ type: { type: 'string' }
206
+ },
207
+ required: ['name', 'value']
208
+ },
209
+ description: 'Array of properties to set'
210
+ }
211
+ },
212
+ required: ['properties']
213
+ }
214
+ },
215
+ {
216
+ name: 'snow_property_export',
217
+ description: 'Export system properties to JSON format',
218
+ inputSchema: {
219
+ type: 'object',
220
+ properties: {
221
+ pattern: {
222
+ type: 'string',
223
+ description: 'Pattern to filter properties (e.g., glide.*)'
224
+ },
225
+ include_system: {
226
+ type: 'boolean',
227
+ description: 'Include system properties',
228
+ default: false
229
+ },
230
+ include_private: {
231
+ type: 'boolean',
232
+ description: 'Include private properties',
233
+ default: false
234
+ }
235
+ }
236
+ }
237
+ },
238
+ {
239
+ name: 'snow_property_import',
240
+ description: 'Import system properties from JSON',
241
+ inputSchema: {
242
+ type: 'object',
243
+ properties: {
244
+ properties: {
245
+ type: 'object',
246
+ description: 'JSON object with property names as keys'
247
+ },
248
+ overwrite: {
249
+ type: 'boolean',
250
+ description: 'Overwrite existing properties',
251
+ default: false
252
+ },
253
+ dry_run: {
254
+ type: 'boolean',
255
+ description: 'Preview changes without applying',
256
+ default: false
257
+ }
258
+ },
259
+ required: ['properties']
260
+ }
261
+ },
262
+ {
263
+ name: 'snow_property_validate',
264
+ description: 'Validate property value against its type and constraints',
265
+ inputSchema: {
266
+ type: 'object',
267
+ properties: {
268
+ name: {
269
+ type: 'string',
270
+ description: 'Property name'
271
+ },
272
+ value: {
273
+ type: 'string',
274
+ description: 'Value to validate'
275
+ }
276
+ },
277
+ required: ['name', 'value']
278
+ }
279
+ },
280
+ {
281
+ name: 'snow_property_categories',
282
+ description: 'List all property categories',
283
+ inputSchema: {
284
+ type: 'object',
285
+ properties: {
286
+ include_counts: {
287
+ type: 'boolean',
288
+ description: 'Include count of properties per category',
289
+ default: true
290
+ }
291
+ }
292
+ }
293
+ },
294
+ {
295
+ name: 'snow_property_history',
296
+ description: 'Get audit history for a property',
297
+ inputSchema: {
298
+ type: 'object',
299
+ properties: {
300
+ name: {
301
+ type: 'string',
302
+ description: 'Property name'
303
+ },
304
+ limit: {
305
+ type: 'number',
306
+ description: 'Number of history records',
307
+ default: 10
308
+ }
309
+ },
310
+ required: ['name']
311
+ }
312
+ }
313
+ ]
314
+ }));
315
+ }
316
+ setupTools() {
317
+ this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
318
+ const { name, arguments: args } = request.params;
319
+ try {
320
+ // Ensure authentication
321
+ const isAuthenticated = await this.client.isAuthenticated();
322
+ if (!isAuthenticated) {
323
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Not authenticated. Please run "snow-flow auth login" first.');
324
+ }
325
+ switch (name) {
326
+ case 'snow_property_get':
327
+ return await this.getProperty(args);
328
+ case 'snow_property_set':
329
+ return await this.setProperty(args);
330
+ case 'snow_property_list':
331
+ return await this.listProperties(args);
332
+ case 'snow_property_delete':
333
+ return await this.deleteProperty(args);
334
+ case 'snow_property_search':
335
+ return await this.searchProperties(args);
336
+ case 'snow_property_bulk_get':
337
+ return await this.bulkGetProperties(args);
338
+ case 'snow_property_bulk_set':
339
+ return await this.bulkSetProperties(args);
340
+ case 'snow_property_export':
341
+ return await this.exportProperties(args);
342
+ case 'snow_property_import':
343
+ return await this.importProperties(args);
344
+ case 'snow_property_validate':
345
+ return await this.validateProperty(args);
346
+ case 'snow_property_categories':
347
+ return await this.getCategories(args);
348
+ case 'snow_property_history':
349
+ return await this.getPropertyHistory(args);
350
+ default:
351
+ throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
352
+ }
353
+ }
354
+ catch (error) {
355
+ logger.error(`Tool execution failed: ${name}`, error);
356
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, error instanceof Error ? error.message : String(error));
357
+ }
358
+ });
359
+ }
360
+ /**
361
+ * Get a system property value
362
+ */
363
+ async getProperty(args) {
364
+ const { name, include_metadata = false } = args;
365
+ logger.info(`Getting property: ${name}`);
366
+ try {
367
+ const response = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
368
+ if (!response.success || !response.data?.result?.length) {
369
+ return {
370
+ content: [{
371
+ type: 'text',
372
+ text: `❌ Property not found: ${name}`
373
+ }]
374
+ };
375
+ }
376
+ const property = response.data.result[0];
377
+ // Cache the property
378
+ this.propertyCache.set(name, property);
379
+ if (include_metadata) {
380
+ return {
381
+ content: [{
382
+ type: 'text',
383
+ text: `📋 **Property: ${name}**
384
+
385
+ **Value:** ${property.value || '(empty)'}
386
+ **Type:** ${property.type || 'string'}
387
+ **Description:** ${property.description || 'No description'}
388
+ **Suffix:** ${property.suffix || 'global'}
389
+ **Private:** ${property.is_private === 'true' ? 'Yes' : 'No'}
390
+ **Choices:** ${property.choices || 'None'}
391
+ **sys_id:** ${property.sys_id}
392
+
393
+ ✅ Property retrieved successfully`
394
+ }]
395
+ };
396
+ }
397
+ else {
398
+ return {
399
+ content: [{
400
+ type: 'text',
401
+ text: property.value || ''
402
+ }]
403
+ };
404
+ }
405
+ }
406
+ catch (error) {
407
+ logger.error('Failed to get property:', error);
408
+ throw error;
409
+ }
410
+ }
411
+ /**
412
+ * Set or create a system property
413
+ */
414
+ async setProperty(args) {
415
+ const { name, value, description, type = 'string', choices, is_private = false, suffix } = args;
416
+ logger.info(`Setting property: ${name} = ${value}`);
417
+ try {
418
+ // Check if property exists
419
+ const existing = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
420
+ let result;
421
+ if (existing.success && existing.data?.result?.length > 0) {
422
+ // Update existing property
423
+ const sys_id = existing.data.result[0].sys_id;
424
+ result = await this.client.updateRecord('sys_properties', sys_id, {
425
+ value,
426
+ ...(description && { description }),
427
+ ...(type && { type }),
428
+ ...(choices && { choices }),
429
+ ...(suffix && { suffix }),
430
+ is_private: is_private ? 'true' : 'false'
431
+ });
432
+ logger.info(`Updated property: ${name}`);
433
+ }
434
+ else {
435
+ // Create new property
436
+ result = await this.client.createRecord('sys_properties', {
437
+ name,
438
+ value,
439
+ description: description || `Created by Snow-Flow`,
440
+ type,
441
+ choices: choices || '',
442
+ is_private: is_private ? 'true' : 'false',
443
+ suffix: suffix || 'global'
444
+ });
445
+ logger.info(`Created new property: ${name}`);
446
+ }
447
+ if (!result.success) {
448
+ throw new Error(`Failed to set property: ${result.error}`);
449
+ }
450
+ // Clear cache
451
+ this.propertyCache.delete(name);
452
+ return {
453
+ content: [{
454
+ type: 'text',
455
+ text: `✅ Property set successfully!
456
+
457
+ **Name:** ${name}
458
+ **Value:** ${value}
459
+ **Type:** ${type}
460
+ ${description ? `**Description:** ${description}` : ''}
461
+ ${choices ? `**Choices:** ${choices}` : ''}
462
+ **Private:** ${is_private ? 'Yes' : 'No'}
463
+
464
+ 💡 Changes take effect immediately in ServiceNow`
465
+ }]
466
+ };
467
+ }
468
+ catch (error) {
469
+ logger.error('Failed to set property:', error);
470
+ throw error;
471
+ }
472
+ }
473
+ /**
474
+ * List system properties
475
+ */
476
+ async listProperties(args) {
477
+ const { pattern, category, is_private, limit = 100, include_values = true } = args;
478
+ logger.info('Listing properties', { pattern, category, limit });
479
+ try {
480
+ let query = '';
481
+ const conditions = [];
482
+ if (pattern) {
483
+ if (pattern.includes('*')) {
484
+ // Convert wildcard to LIKE query
485
+ const likePattern = pattern.replace(/\*/g, '');
486
+ conditions.push(`nameLIKE${likePattern}`);
487
+ }
488
+ else {
489
+ conditions.push(`name=${pattern}`);
490
+ }
491
+ }
492
+ if (category) {
493
+ conditions.push(`suffix=${category}`);
494
+ }
495
+ if (is_private !== undefined) {
496
+ conditions.push(`is_private=${is_private ? 'true' : 'false'}`);
497
+ }
498
+ query = conditions.join('^');
499
+ const response = await this.client.searchRecords('sys_properties', query, limit);
500
+ if (!response.success || !response.data?.result) {
501
+ throw new Error('Failed to list properties');
502
+ }
503
+ const properties = response.data.result;
504
+ // Group by category/suffix
505
+ const grouped = {};
506
+ for (const prop of properties) {
507
+ const category = prop.suffix || 'global';
508
+ if (!grouped[category])
509
+ grouped[category] = [];
510
+ grouped[category].push(prop);
511
+ }
512
+ let output = `📋 **System Properties** (Found: ${properties.length})\n\n`;
513
+ for (const [cat, props] of Object.entries(grouped)) {
514
+ output += `**Category: ${cat}**\n`;
515
+ for (const prop of props) {
516
+ if (include_values) {
517
+ output += `• ${prop.name} = "${prop.value || ''}"\n`;
518
+ if (prop.description) {
519
+ output += ` ↳ ${prop.description}\n`;
520
+ }
521
+ }
522
+ else {
523
+ output += `• ${prop.name}\n`;
524
+ }
525
+ }
526
+ output += '\n';
527
+ }
528
+ return {
529
+ content: [{
530
+ type: 'text',
531
+ text: output
532
+ }]
533
+ };
534
+ }
535
+ catch (error) {
536
+ logger.error('Failed to list properties:', error);
537
+ throw error;
538
+ }
539
+ }
540
+ /**
541
+ * Delete a system property
542
+ */
543
+ async deleteProperty(args) {
544
+ const { name, confirm } = args;
545
+ if (!confirm) {
546
+ return {
547
+ content: [{
548
+ type: 'text',
549
+ text: `⚠️ Deletion requires confirmation. Set confirm: true to proceed.
550
+
551
+ **Property to delete:** ${name}
552
+
553
+ ⚠️ WARNING: Deleting system properties can affect ServiceNow functionality!`
554
+ }]
555
+ };
556
+ }
557
+ logger.info(`Deleting property: ${name}`);
558
+ try {
559
+ // Find the property
560
+ const response = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
561
+ if (!response.success || !response.data?.result?.length) {
562
+ return {
563
+ content: [{
564
+ type: 'text',
565
+ text: `❌ Property not found: ${name}`
566
+ }]
567
+ };
568
+ }
569
+ const sys_id = response.data.result[0].sys_id;
570
+ const result = await this.client.deleteRecord('sys_properties', sys_id);
571
+ if (!result.success) {
572
+ throw new Error(`Failed to delete property: ${result.error}`);
573
+ }
574
+ // Clear cache
575
+ this.propertyCache.delete(name);
576
+ return {
577
+ content: [{
578
+ type: 'text',
579
+ text: `✅ Property deleted successfully: ${name}
580
+
581
+ ⚠️ Note: Some properties may be recreated by ServiceNow on next access with default values.`
582
+ }]
583
+ };
584
+ }
585
+ catch (error) {
586
+ logger.error('Failed to delete property:', error);
587
+ throw error;
588
+ }
589
+ }
590
+ /**
591
+ * Search properties
592
+ */
593
+ async searchProperties(args) {
594
+ const { search_term, search_in = 'all', limit = 50 } = args;
595
+ logger.info(`Searching properties for: ${search_term}`);
596
+ try {
597
+ let query = '';
598
+ switch (search_in) {
599
+ case 'name':
600
+ query = `nameLIKE${search_term}`;
601
+ break;
602
+ case 'value':
603
+ query = `valueLIKE${search_term}`;
604
+ break;
605
+ case 'description':
606
+ query = `descriptionLIKE${search_term}`;
607
+ break;
608
+ case 'all':
609
+ default:
610
+ query = `nameLIKE${search_term}^ORvalueLIKE${search_term}^ORdescriptionLIKE${search_term}`;
611
+ }
612
+ const response = await this.client.searchRecords('sys_properties', query, limit);
613
+ if (!response.success || !response.data?.result) {
614
+ throw new Error('Search failed');
615
+ }
616
+ const results = response.data.result;
617
+ if (results.length === 0) {
618
+ return {
619
+ content: [{
620
+ type: 'text',
621
+ text: `No properties found matching: "${search_term}"`
622
+ }]
623
+ };
624
+ }
625
+ let output = `🔍 **Search Results** (Found: ${results.length})\n`;
626
+ output += `Search term: "${search_term}" in ${search_in}\n\n`;
627
+ for (const prop of results) {
628
+ output += `**${prop.name}**\n`;
629
+ output += `• Value: ${prop.value || '(empty)'}\n`;
630
+ if (prop.description) {
631
+ output += `• Description: ${prop.description}\n`;
632
+ }
633
+ output += '\n';
634
+ }
635
+ return {
636
+ content: [{
637
+ type: 'text',
638
+ text: output
639
+ }]
640
+ };
641
+ }
642
+ catch (error) {
643
+ logger.error('Search failed:', error);
644
+ throw error;
645
+ }
646
+ }
647
+ /**
648
+ * Bulk get properties
649
+ */
650
+ async bulkGetProperties(args) {
651
+ const { names, include_metadata = false } = args;
652
+ logger.info(`Bulk getting ${names.length} properties`);
653
+ const results = {};
654
+ const errors = [];
655
+ for (const name of names) {
656
+ try {
657
+ // Check cache first
658
+ if (this.propertyCache.has(name)) {
659
+ results[name] = this.propertyCache.get(name);
660
+ continue;
661
+ }
662
+ const response = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
663
+ if (response.success && response.data?.result?.length > 0) {
664
+ const prop = response.data.result[0];
665
+ results[name] = include_metadata ? prop : prop.value;
666
+ this.propertyCache.set(name, prop);
667
+ }
668
+ else {
669
+ results[name] = null;
670
+ errors.push(name);
671
+ }
672
+ }
673
+ catch (error) {
674
+ logger.error(`Failed to get property ${name}:`, error);
675
+ results[name] = null;
676
+ errors.push(name);
677
+ }
678
+ }
679
+ let output = `📋 **Bulk Property Retrieval**\n\n`;
680
+ if (include_metadata) {
681
+ output += JSON.stringify(results, null, 2);
682
+ }
683
+ else {
684
+ for (const [name, value] of Object.entries(results)) {
685
+ output += `• ${name} = ${value !== null ? `"${value}"` : 'NOT FOUND'}\n`;
686
+ }
687
+ }
688
+ if (errors.length > 0) {
689
+ output += `\n⚠️ Properties not found: ${errors.join(', ')}`;
690
+ }
691
+ return {
692
+ content: [{
693
+ type: 'text',
694
+ text: output
695
+ }]
696
+ };
697
+ }
698
+ /**
699
+ * Bulk set properties
700
+ */
701
+ async bulkSetProperties(args) {
702
+ const { properties } = args;
703
+ logger.info(`Bulk setting ${properties.length} properties`);
704
+ const results = {
705
+ created: [],
706
+ updated: [],
707
+ failed: []
708
+ };
709
+ for (const prop of properties) {
710
+ try {
711
+ // Check if exists
712
+ const existing = await this.client.searchRecords('sys_properties', `name=${prop.name}`, 1);
713
+ let result;
714
+ if (existing.success && existing.data?.result?.length > 0) {
715
+ // Update
716
+ const sys_id = existing.data.result[0].sys_id;
717
+ result = await this.client.updateRecord('sys_properties', sys_id, {
718
+ value: prop.value,
719
+ ...(prop.description && { description: prop.description }),
720
+ ...(prop.type && { type: prop.type })
721
+ });
722
+ if (result.success) {
723
+ results.updated.push(prop.name);
724
+ }
725
+ else {
726
+ results.failed.push(`${prop.name}: ${result.error}`);
727
+ }
728
+ }
729
+ else {
730
+ // Create
731
+ result = await this.client.createRecord('sys_properties', {
732
+ name: prop.name,
733
+ value: prop.value,
734
+ description: prop.description || `Created by Snow-Flow bulk operation`,
735
+ type: prop.type || 'string'
736
+ });
737
+ if (result.success) {
738
+ results.created.push(prop.name);
739
+ }
740
+ else {
741
+ results.failed.push(`${prop.name}: ${result.error}`);
742
+ }
743
+ }
744
+ // Clear cache
745
+ this.propertyCache.delete(prop.name);
746
+ }
747
+ catch (error) {
748
+ logger.error(`Failed to set property ${prop.name}:`, error);
749
+ results.failed.push(`${prop.name}: ${error}`);
750
+ }
751
+ }
752
+ return {
753
+ content: [{
754
+ type: 'text',
755
+ text: `📦 **Bulk Property Update Results**
756
+
757
+ ✅ **Created:** ${results.created.length}
758
+ ${results.created.map(n => `• ${n}`).join('\n')}
759
+
760
+ 🔄 **Updated:** ${results.updated.length}
761
+ ${results.updated.map(n => `• ${n}`).join('\n')}
762
+
763
+ ${results.failed.length > 0 ? `❌ **Failed:** ${results.failed.length}\n${results.failed.map(f => `• ${f}`).join('\n')}` : ''}
764
+
765
+ Total processed: ${properties.length}`
766
+ }]
767
+ };
768
+ }
769
+ /**
770
+ * Export properties
771
+ */
772
+ async exportProperties(args) {
773
+ const { pattern, include_system = false, include_private = false } = args;
774
+ logger.info('Exporting properties', { pattern, include_system, include_private });
775
+ try {
776
+ let query = '';
777
+ const conditions = [];
778
+ if (pattern) {
779
+ if (pattern.includes('*')) {
780
+ const likePattern = pattern.replace(/\*/g, '');
781
+ conditions.push(`nameLIKE${likePattern}`);
782
+ }
783
+ else {
784
+ conditions.push(`name=${pattern}`);
785
+ }
786
+ }
787
+ if (!include_system) {
788
+ conditions.push(`name!=glide.*^name!=sys.*`);
789
+ }
790
+ if (!include_private) {
791
+ conditions.push(`is_private=false`);
792
+ }
793
+ query = conditions.join('^');
794
+ const response = await this.client.searchRecords('sys_properties', query, 1000);
795
+ if (!response.success || !response.data?.result) {
796
+ throw new Error('Export failed');
797
+ }
798
+ const properties = response.data.result;
799
+ const exportData = {};
800
+ for (const prop of properties) {
801
+ exportData[prop.name] = {
802
+ value: prop.value,
803
+ type: prop.type || 'string',
804
+ description: prop.description || '',
805
+ suffix: prop.suffix || 'global',
806
+ is_private: prop.is_private === 'true',
807
+ choices: prop.choices || ''
808
+ };
809
+ }
810
+ return {
811
+ content: [{
812
+ type: 'text',
813
+ text: `📤 **Properties Export** (${properties.length} properties)
814
+
815
+ \`\`\`json
816
+ ${JSON.stringify(exportData, null, 2)}
817
+ \`\`\`
818
+
819
+ ✅ Export complete. You can save this JSON for backup or migration.`
820
+ }]
821
+ };
822
+ }
823
+ catch (error) {
824
+ logger.error('Export failed:', error);
825
+ throw error;
826
+ }
827
+ }
828
+ /**
829
+ * Import properties
830
+ */
831
+ async importProperties(args) {
832
+ const { properties, overwrite = false, dry_run = false } = args;
833
+ logger.info('Importing properties', { count: Object.keys(properties).length, overwrite, dry_run });
834
+ const results = {
835
+ would_create: [],
836
+ would_update: [],
837
+ would_skip: [],
838
+ created: [],
839
+ updated: [],
840
+ skipped: [],
841
+ failed: []
842
+ };
843
+ for (const [name, data] of Object.entries(properties)) {
844
+ try {
845
+ // Check if exists
846
+ const existing = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
847
+ const exists = existing.success && existing.data?.result?.length > 0;
848
+ if (dry_run) {
849
+ if (exists && overwrite) {
850
+ results.would_update.push(name);
851
+ }
852
+ else if (exists && !overwrite) {
853
+ results.would_skip.push(name);
854
+ }
855
+ else {
856
+ results.would_create.push(name);
857
+ }
858
+ continue;
859
+ }
860
+ if (exists && !overwrite) {
861
+ results.skipped.push(name);
862
+ continue;
863
+ }
864
+ const propertyData = typeof data === 'object' ? data : { value: data };
865
+ if (exists) {
866
+ // Update
867
+ const sys_id = existing.data.result[0].sys_id;
868
+ const result = await this.client.updateRecord('sys_properties', sys_id, {
869
+ value: propertyData.value,
870
+ ...(propertyData.description && { description: propertyData.description }),
871
+ ...(propertyData.type && { type: propertyData.type }),
872
+ ...(propertyData.suffix && { suffix: propertyData.suffix }),
873
+ ...(propertyData.choices && { choices: propertyData.choices }),
874
+ ...(propertyData.is_private !== undefined && { is_private: propertyData.is_private ? 'true' : 'false' })
875
+ });
876
+ if (result.success) {
877
+ results.updated.push(name);
878
+ }
879
+ else {
880
+ results.failed.push(`${name}: ${result.error}`);
881
+ }
882
+ }
883
+ else {
884
+ // Create
885
+ const result = await this.client.createRecord('sys_properties', {
886
+ name,
887
+ value: propertyData.value,
888
+ description: propertyData.description || `Imported by Snow-Flow`,
889
+ type: propertyData.type || 'string',
890
+ suffix: propertyData.suffix || 'global',
891
+ choices: propertyData.choices || '',
892
+ is_private: propertyData.is_private ? 'true' : 'false'
893
+ });
894
+ if (result.success) {
895
+ results.created.push(name);
896
+ }
897
+ else {
898
+ results.failed.push(`${name}: ${result.error}`);
899
+ }
900
+ }
901
+ // Clear cache
902
+ this.propertyCache.delete(name);
903
+ }
904
+ catch (error) {
905
+ logger.error(`Failed to import property ${name}:`, error);
906
+ results.failed.push(`${name}: ${error}`);
907
+ }
908
+ }
909
+ if (dry_run) {
910
+ return {
911
+ content: [{
912
+ type: 'text',
913
+ text: `🔍 **Import Preview (Dry Run)**
914
+
915
+ Would create: ${results.would_create.length}
916
+ ${results.would_create.slice(0, 10).map(n => `• ${n}`).join('\n')}${results.would_create.length > 10 ? `\n... and ${results.would_create.length - 10} more` : ''}
917
+
918
+ Would update: ${results.would_update.length}
919
+ ${results.would_update.slice(0, 10).map(n => `• ${n}`).join('\n')}${results.would_update.length > 10 ? `\n... and ${results.would_update.length - 10} more` : ''}
920
+
921
+ Would skip: ${results.would_skip.length}
922
+ ${results.would_skip.slice(0, 10).map(n => `• ${n}`).join('\n')}${results.would_skip.length > 10 ? `\n... and ${results.would_skip.length - 10} more` : ''}
923
+
924
+ ✅ Run with dry_run: false to apply changes`
925
+ }]
926
+ };
927
+ }
928
+ return {
929
+ content: [{
930
+ type: 'text',
931
+ text: `📥 **Import Results**
932
+
933
+ ✅ Created: ${results.created.length}
934
+ 🔄 Updated: ${results.updated.length}
935
+ ⏭️ Skipped: ${results.skipped.length}
936
+ ${results.failed.length > 0 ? `❌ Failed: ${results.failed.length}\n${results.failed.join('\n')}` : ''}
937
+
938
+ Total processed: ${Object.keys(properties).length}`
939
+ }]
940
+ };
941
+ }
942
+ /**
943
+ * Validate property value
944
+ */
945
+ async validateProperty(args) {
946
+ const { name, value } = args;
947
+ logger.info(`Validating property: ${name} = ${value}`);
948
+ try {
949
+ // Get property metadata
950
+ const response = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
951
+ if (!response.success || !response.data?.result?.length) {
952
+ return {
953
+ content: [{
954
+ type: 'text',
955
+ text: `❌ Property not found: ${name}. Cannot validate.`
956
+ }]
957
+ };
958
+ }
959
+ const property = response.data.result[0];
960
+ const validationResults = [];
961
+ let isValid = true;
962
+ // Type validation
963
+ if (property.type) {
964
+ switch (property.type) {
965
+ case 'boolean':
966
+ if (!['true', 'false', '1', '0'].includes(value.toLowerCase())) {
967
+ validationResults.push('❌ Value must be true/false');
968
+ isValid = false;
969
+ }
970
+ else {
971
+ validationResults.push('✅ Valid boolean value');
972
+ }
973
+ break;
974
+ case 'integer':
975
+ if (!/^-?\d+$/.test(value)) {
976
+ validationResults.push('❌ Value must be an integer');
977
+ isValid = false;
978
+ }
979
+ else {
980
+ validationResults.push('✅ Valid integer value');
981
+ }
982
+ break;
983
+ case 'float':
984
+ case 'decimal':
985
+ if (!/^-?\d*\.?\d+$/.test(value)) {
986
+ validationResults.push('❌ Value must be a number');
987
+ isValid = false;
988
+ }
989
+ else {
990
+ validationResults.push('✅ Valid numeric value');
991
+ }
992
+ break;
993
+ case 'string':
994
+ default:
995
+ validationResults.push('✅ Valid string value');
996
+ }
997
+ }
998
+ // Choices validation
999
+ if (property.choices) {
1000
+ const validChoices = property.choices.split(',').map((c) => c.trim());
1001
+ if (!validChoices.includes(value)) {
1002
+ validationResults.push(`❌ Value must be one of: ${validChoices.join(', ')}`);
1003
+ isValid = false;
1004
+ }
1005
+ else {
1006
+ validationResults.push('✅ Valid choice');
1007
+ }
1008
+ }
1009
+ return {
1010
+ content: [{
1011
+ type: 'text',
1012
+ text: `🔍 **Property Validation: ${name}**
1013
+
1014
+ **Current Value:** ${property.value}
1015
+ **New Value:** ${value}
1016
+ **Type:** ${property.type || 'string'}
1017
+ ${property.choices ? `**Valid Choices:** ${property.choices}` : ''}
1018
+
1019
+ **Validation Results:**
1020
+ ${validationResults.join('\n')}
1021
+
1022
+ **Overall:** ${isValid ? '✅ VALID' : '❌ INVALID'}`
1023
+ }]
1024
+ };
1025
+ }
1026
+ catch (error) {
1027
+ logger.error('Validation failed:', error);
1028
+ throw error;
1029
+ }
1030
+ }
1031
+ /**
1032
+ * Get property categories
1033
+ */
1034
+ async getCategories(args) {
1035
+ const { include_counts = true } = args;
1036
+ logger.info('Getting property categories');
1037
+ try {
1038
+ // Get distinct suffixes (categories)
1039
+ const response = await this.client.searchRecords('sys_properties', '', 1000);
1040
+ if (!response.success || !response.data?.result) {
1041
+ throw new Error('Failed to get categories');
1042
+ }
1043
+ const categories = {};
1044
+ for (const prop of response.data.result) {
1045
+ const category = prop.suffix || 'global';
1046
+ categories[category] = (categories[category] || 0) + 1;
1047
+ }
1048
+ const sorted = Object.entries(categories).sort((a, b) => b[1] - a[1]);
1049
+ let output = `📂 **Property Categories**\n\n`;
1050
+ for (const [category, count] of sorted) {
1051
+ if (include_counts) {
1052
+ output += `• **${category}** (${count} properties)\n`;
1053
+ }
1054
+ else {
1055
+ output += `• ${category}\n`;
1056
+ }
1057
+ }
1058
+ output += `\n📊 Total categories: ${sorted.length}`;
1059
+ output += `\n📋 Total properties: ${response.data.result.length}`;
1060
+ return {
1061
+ content: [{
1062
+ type: 'text',
1063
+ text: output
1064
+ }]
1065
+ };
1066
+ }
1067
+ catch (error) {
1068
+ logger.error('Failed to get categories:', error);
1069
+ throw error;
1070
+ }
1071
+ }
1072
+ /**
1073
+ * Get property audit history
1074
+ */
1075
+ async getPropertyHistory(args) {
1076
+ const { name, limit = 10 } = args;
1077
+ logger.info(`Getting history for property: ${name}`);
1078
+ try {
1079
+ // First, get the property to get its sys_id
1080
+ const propResponse = await this.client.searchRecords('sys_properties', `name=${name}`, 1);
1081
+ if (!propResponse.success || !propResponse.data?.result?.length) {
1082
+ return {
1083
+ content: [{
1084
+ type: 'text',
1085
+ text: `❌ Property not found: ${name}`
1086
+ }]
1087
+ };
1088
+ }
1089
+ const sys_id = propResponse.data.result[0].sys_id;
1090
+ // Get audit history
1091
+ const auditResponse = await this.client.searchRecords('sys_audit', `documentkey=${sys_id}^tablename=sys_properties`, limit);
1092
+ if (!auditResponse.success || !auditResponse.data?.result?.length) {
1093
+ return {
1094
+ content: [{
1095
+ type: 'text',
1096
+ text: `📜 No audit history found for property: ${name}`
1097
+ }]
1098
+ };
1099
+ }
1100
+ let output = `📜 **Audit History: ${name}**\n\n`;
1101
+ for (const audit of auditResponse.data.result) {
1102
+ output += `**${audit.sys_created_on}**\n`;
1103
+ output += `• User: ${audit.sys_created_by}\n`;
1104
+ output += `• Field: ${audit.fieldname}\n`;
1105
+ output += `• Old: ${audit.oldvalue || '(empty)'}\n`;
1106
+ output += `• New: ${audit.newvalue || '(empty)'}\n`;
1107
+ output += '\n';
1108
+ }
1109
+ return {
1110
+ content: [{
1111
+ type: 'text',
1112
+ text: output
1113
+ }]
1114
+ };
1115
+ }
1116
+ catch (error) {
1117
+ logger.error('Failed to get history:', error);
1118
+ // Audit might not be available
1119
+ return {
1120
+ content: [{
1121
+ type: 'text',
1122
+ text: `⚠️ Audit history not available for this property or table.
1123
+
1124
+ Note: Audit history requires sys_audit to be enabled for sys_properties table.`
1125
+ }]
1126
+ };
1127
+ }
1128
+ }
1129
+ async start() {
1130
+ const transport = new stdio_js_1.StdioServerTransport();
1131
+ await this.server.connect(transport);
1132
+ logger.info('ServiceNow System Properties MCP Server started');
1133
+ }
1134
+ }
1135
+ exports.ServiceNowSystemPropertiesMCP = ServiceNowSystemPropertiesMCP;
1136
+ // Start the server
1137
+ const server = new ServiceNowSystemPropertiesMCP();
1138
+ server.start().catch((error) => {
1139
+ console.error('Failed to start ServiceNow System Properties MCP:', error);
1140
+ process.exit(1);
1141
+ });
1142
+ //# sourceMappingURL=servicenow-system-properties-mcp.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "snow-flow",
3
- "version": "3.3.7",
4
- "description": "Snow-Flow v3.3.7: PORTAL PAGE FIX - Fixed portal_page deployment field mapping! Now correctly maps id->page_id, handles sp_portal sys_id, and converts containers to widgets format. Fixed widget instance creation with proper sys_id handling. Portal pages with AI chatbots and complex widgets now deploy successfully. Improved validation messages and support for both single widgets and widget arrays. 180+ MCP tools across 17 specialized servers.",
3
+ "version": "3.3.8",
4
+ "description": "Snow-Flow v3.3.8: SYSTEM PROPERTIES MCP - Complete system property management via official ServiceNow APIs! 12 new tools: get, set, list, delete, search, bulk operations, import/export, validation, categories, and audit history. All using standard Table API on sys_properties - no hacks, 100% official! Manage configurations, feature flags, and system settings programmatically. Now 192+ MCP tools across 18 specialized servers.",
5
5
  "main": "dist/index.js",
6
6
  "type": "commonjs",
7
7
  "bin": {
@@ -27,6 +27,7 @@
27
27
  "mcp:clean": "node scripts/cleanup-mcp-servers.js && npm run build",
28
28
  "mcp:start": "node scripts/start-mcp-proper.js",
29
29
  "mcp:start-proper": "node scripts/start-mcp-proper.js",
30
+ "mcp:sysprops": "node scripts/start-sysprops-mcp.js",
30
31
  "test:integration": "npm run build && node dist/tests/integration-test.js",
31
32
  "postbuild-disabled": "npm run setup-mcp",
32
33
  "postinstall": "node scripts/postinstall.js",