snow-flow 2.8.9 → 2.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/check-mcp-resources.sh +85 -0
- package/dist/config/snow-flow-config.js +1 -1
- package/dist/mcp/mcp-on-demand-proxy.d.ts +7 -0
- package/dist/mcp/mcp-on-demand-proxy.js +187 -0
- package/dist/mcp/servicenow-machine-learning-mcp.js +310 -60
- package/dist/mcp/servicenow-operations-mcp.d.ts +71 -1
- package/dist/mcp/servicenow-operations-mcp.js +44 -11
- package/dist/test-smart-limits.d.ts +13 -0
- package/dist/test-smart-limits.js +100 -0
- package/dist/utils/mcp-on-demand-manager.d.ts +69 -0
- package/dist/utils/mcp-on-demand-manager.js +309 -0
- package/dist/utils/mcp-process-manager.d.ts +58 -0
- package/dist/utils/mcp-process-manager.js +220 -0
- package/dist/utils/mcp-server-manager.js +50 -9
- package/dist/utils/mcp-singleton-lock.d.ts +4 -0
- package/dist/utils/mcp-singleton-lock.js +35 -8
- package/dist/utils/ml-data-fetcher.d.ts +66 -0
- package/dist/utils/ml-data-fetcher.js +288 -0
- package/dist/utils/servicenow-client.js +43 -17
- package/dist/utils/timeout-manager.d.ts +62 -0
- package/dist/utils/timeout-manager.js +352 -0
- package/package.json +1 -1
- package/test-ml-improvements.sh +76 -0
|
@@ -44,6 +44,7 @@ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
|
44
44
|
const tf = __importStar(require("@tensorflow/tfjs-node"));
|
|
45
45
|
const logger_js_1 = require("../utils/logger.js");
|
|
46
46
|
const servicenow_client_js_1 = require("../utils/servicenow-client.js");
|
|
47
|
+
const ml_data_fetcher_js_1 = require("../utils/ml-data-fetcher.js");
|
|
47
48
|
class ServiceNowMachineLearningMCP {
|
|
48
49
|
constructor(credentials) {
|
|
49
50
|
// Model cache
|
|
@@ -108,14 +109,18 @@ class ServiceNowMachineLearningMCP {
|
|
|
108
109
|
// Training tools
|
|
109
110
|
{
|
|
110
111
|
name: 'ml_train_incident_classifier',
|
|
111
|
-
description: 'Train LSTM neural network on historical incident data with INTELLIGENT data selection. Snow-Flow automatically
|
|
112
|
+
description: 'Train LSTM neural network on historical incident data with INTELLIGENT data selection. Snow-Flow automatically detects available data and uses the optimal amount (up to 5000) for best accuracy. Works WITHOUT PA/PI plugins - only needs incident table access!',
|
|
112
113
|
inputSchema: {
|
|
113
114
|
type: 'object',
|
|
114
115
|
properties: {
|
|
115
116
|
sample_size: {
|
|
116
117
|
type: 'number',
|
|
117
|
-
description: 'Number of incidents to use for training'
|
|
118
|
-
|
|
118
|
+
description: 'Number of incidents to use for training. If not specified, automatically uses all available data (up to 5000). Set to limit training data.'
|
|
119
|
+
},
|
|
120
|
+
auto_maximize_data: {
|
|
121
|
+
type: 'boolean',
|
|
122
|
+
description: 'Automatically use all available incident data for best model accuracy (default: true)',
|
|
123
|
+
default: true
|
|
119
124
|
},
|
|
120
125
|
epochs: {
|
|
121
126
|
type: 'number',
|
|
@@ -134,7 +139,7 @@ class ServiceNowMachineLearningMCP {
|
|
|
134
139
|
},
|
|
135
140
|
intelligent_selection: {
|
|
136
141
|
type: 'boolean',
|
|
137
|
-
description: 'Let Snow-Flow intelligently select balanced training data across categories, priorities, and time periods',
|
|
142
|
+
description: 'Let Snow-Flow intelligently select balanced training data across categories, priorities, and time periods. Combined with auto_maximize_data for optimal results.',
|
|
138
143
|
default: true
|
|
139
144
|
},
|
|
140
145
|
focus_categories: {
|
|
@@ -505,7 +510,9 @@ class ServiceNowMachineLearningMCP {
|
|
|
505
510
|
* Uses PI if available, otherwise uses custom TensorFlow.js
|
|
506
511
|
*/
|
|
507
512
|
async trainIncidentClassifier(args) {
|
|
508
|
-
const { sample_size
|
|
513
|
+
const { sample_size, // No default - will be determined dynamically
|
|
514
|
+
epochs = 50, validation_split = 0.2, query = '', intelligent_selection = true, focus_categories = [], batch_size = 100, streaming_mode = true, auto_maximize_data = true // New option to automatically use all available data
|
|
515
|
+
} = args;
|
|
509
516
|
// CRITICAL FIX: Ensure max_vocabulary_size is ALWAYS valid
|
|
510
517
|
const max_vocabulary_size = Math.max(1000, args.max_vocabulary_size || 5000);
|
|
511
518
|
try {
|
|
@@ -513,6 +520,91 @@ class ServiceNowMachineLearningMCP {
|
|
|
513
520
|
if (!this.mlAPICheckComplete) {
|
|
514
521
|
await this.checkMLAPIAvailability();
|
|
515
522
|
}
|
|
523
|
+
// First, check how much data is available
|
|
524
|
+
let actualSampleSize = sample_size || 2000; // Default to 2000 if not specified
|
|
525
|
+
if (auto_maximize_data || !sample_size) {
|
|
526
|
+
this.logger.info('🔍 Checking available incident data for optimal training...');
|
|
527
|
+
try {
|
|
528
|
+
// Count available incidents that match our criteria
|
|
529
|
+
const countQuery = query || (intelligent_selection ?
|
|
530
|
+
'categoryISNOTEMPTY^descriptionISNOTEMPTY^sys_created_onONLast 6 months' :
|
|
531
|
+
'');
|
|
532
|
+
// Use ServiceNow aggregate API to count records efficiently
|
|
533
|
+
let totalAvailable = 0;
|
|
534
|
+
try {
|
|
535
|
+
// Try using the stats API first (most efficient)
|
|
536
|
+
const statsResponse = await this.makeServiceNowRequest('/api/now/stats/incident', {
|
|
537
|
+
sysparm_query: countQuery,
|
|
538
|
+
sysparm_count: true
|
|
539
|
+
});
|
|
540
|
+
if (statsResponse.data?.result?.stats?.count) {
|
|
541
|
+
totalAvailable = parseInt(statsResponse.data.result.stats.count);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch (statsError) {
|
|
545
|
+
// Fallback: Use aggregate API
|
|
546
|
+
try {
|
|
547
|
+
const aggResponse = await this.makeServiceNowRequest('/api/now/table/incident', {
|
|
548
|
+
sysparm_query: countQuery,
|
|
549
|
+
sysparm_count: true,
|
|
550
|
+
sysparm_limit: 1
|
|
551
|
+
});
|
|
552
|
+
// ServiceNow returns count in result
|
|
553
|
+
if (aggResponse?.result && Array.isArray(aggResponse.result)) {
|
|
554
|
+
// Even with limit 1, we can estimate based on typical data
|
|
555
|
+
totalAvailable = 2000; // Conservative estimate when count API fails
|
|
556
|
+
this.logger.info('Using conservative estimate of 2000 incidents');
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch (aggError) {
|
|
560
|
+
// Final fallback: Estimate based on a sample
|
|
561
|
+
const sampleResult = await this.client.searchRecords('incident', countQuery, 1000);
|
|
562
|
+
if (sampleResult.success && sampleResult.data?.result) {
|
|
563
|
+
totalAvailable = sampleResult.data.result.length >= 1000 ? 5000 : sampleResult.data.result.length;
|
|
564
|
+
this.logger.info(`Estimated ${totalAvailable} incidents available (sampled)`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
// Use a reasonable maximum (5000) to avoid memory issues
|
|
569
|
+
const maxRecommended = 5000;
|
|
570
|
+
const optimalSize = Math.min(totalAvailable, maxRecommended);
|
|
571
|
+
if (totalAvailable > 0) {
|
|
572
|
+
this.logger.info(`📊 Found ${totalAvailable} incidents available for training`);
|
|
573
|
+
if (sample_size && sample_size > totalAvailable) {
|
|
574
|
+
this.logger.warn(`⚠️ Requested ${sample_size} samples but only ${totalAvailable} available`);
|
|
575
|
+
}
|
|
576
|
+
// Use the optimal amount of data
|
|
577
|
+
actualSampleSize = sample_size ?
|
|
578
|
+
Math.min(sample_size, totalAvailable) :
|
|
579
|
+
optimalSize;
|
|
580
|
+
this.logger.info(`✅ Using ${actualSampleSize} incidents for training (optimal for this dataset)`);
|
|
581
|
+
// Provide recommendations based on data size
|
|
582
|
+
if (actualSampleSize < 500) {
|
|
583
|
+
this.logger.warn('⚠️ Less than 500 samples - model accuracy may be limited');
|
|
584
|
+
this.logger.info('💡 Recommendation: Gather more incident data for better results');
|
|
585
|
+
}
|
|
586
|
+
else if (actualSampleSize < 1000) {
|
|
587
|
+
this.logger.info('📈 Moderate dataset - expect 70-80% accuracy');
|
|
588
|
+
}
|
|
589
|
+
else if (actualSampleSize < 2000) {
|
|
590
|
+
this.logger.info('📈 Good dataset - expect 80-85% accuracy');
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
this.logger.info('🎯 Excellent dataset - expect 85-95% accuracy');
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
// Fallback to a default if count fails
|
|
598
|
+
actualSampleSize = sample_size || 1000;
|
|
599
|
+
this.logger.info(`Using default sample size: ${actualSampleSize}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
// If counting fails, use the provided or default size
|
|
604
|
+
actualSampleSize = sample_size || 1000;
|
|
605
|
+
this.logger.warn('Could not determine available data, using:', actualSampleSize);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
516
608
|
// If PI is available, try to use it first
|
|
517
609
|
if (this.hasPI) {
|
|
518
610
|
try {
|
|
@@ -553,7 +645,7 @@ class ServiceNowMachineLearningMCP {
|
|
|
553
645
|
// 🔴 CRITICAL FIX: Use full sample_size, not artificially limited amount
|
|
554
646
|
let incidents = [];
|
|
555
647
|
try {
|
|
556
|
-
incidents = await this.fetchIncidentData(
|
|
648
|
+
incidents = await this.fetchIncidentData(actualSampleSize, {
|
|
557
649
|
query,
|
|
558
650
|
intelligent_selection,
|
|
559
651
|
focus_categories
|
|
@@ -1169,40 +1261,115 @@ class ServiceNowMachineLearningMCP {
|
|
|
1169
1261
|
else if (!finalQuery) {
|
|
1170
1262
|
finalQuery = 'ORDERBYDESCsys_created_on';
|
|
1171
1263
|
}
|
|
1172
|
-
// Use
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1264
|
+
// Use smart ML data fetcher for batched retrieval to avoid token limits
|
|
1265
|
+
this.logger.info(`🤖 Using smart ML data fetcher for ${limit} incidents`);
|
|
1266
|
+
try {
|
|
1267
|
+
// Create a delegate for the operations MCP
|
|
1268
|
+
const operationsMCP = {
|
|
1269
|
+
handleTool: async (toolName, args) => {
|
|
1270
|
+
if (toolName === 'snow_query_table') {
|
|
1271
|
+
const { table, query: q, limit: l, fields, include_content } = args;
|
|
1272
|
+
// Use the client to fetch data
|
|
1273
|
+
const response = await this.client.searchRecords(table, q || '', l || 100);
|
|
1274
|
+
if (!response.success) {
|
|
1275
|
+
throw new Error(`Query failed: ${response.error}`);
|
|
1276
|
+
}
|
|
1277
|
+
const records = response.data?.result || [];
|
|
1278
|
+
// Filter fields if specified
|
|
1279
|
+
let filteredRecords = records;
|
|
1280
|
+
if (fields && fields.length > 0) {
|
|
1281
|
+
filteredRecords = records.map((record) => {
|
|
1282
|
+
const filtered = {};
|
|
1283
|
+
for (const field of fields) {
|
|
1284
|
+
if (field in record) {
|
|
1285
|
+
filtered[field] = record[field];
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return filtered;
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
// Format response
|
|
1292
|
+
if (include_content) {
|
|
1293
|
+
return {
|
|
1294
|
+
content: [{
|
|
1295
|
+
type: 'text',
|
|
1296
|
+
text: JSON.stringify(filteredRecords, null, 2)
|
|
1297
|
+
}]
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
else {
|
|
1301
|
+
return {
|
|
1302
|
+
content: [{
|
|
1303
|
+
type: 'text',
|
|
1304
|
+
text: `Found ${records.length} ${table} records matching query: "${q || 'all'}"`
|
|
1305
|
+
}]
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
const dataFetcher = new ml_data_fetcher_js_1.MLDataFetcher(operationsMCP);
|
|
1313
|
+
const result = await dataFetcher.smartFetch({
|
|
1314
|
+
table: 'incident',
|
|
1315
|
+
query: finalQuery,
|
|
1316
|
+
totalSamples: limit,
|
|
1317
|
+
batchSize: 50, // Small batches to avoid token limits
|
|
1318
|
+
discoverFields: true,
|
|
1319
|
+
includeContent: true
|
|
1189
1320
|
});
|
|
1190
|
-
this.logger.info(
|
|
1191
|
-
this.logger.info(
|
|
1192
|
-
|
|
1321
|
+
this.logger.info(`🎉 Fetched ${result.totalFetched} incidents in ${result.batchesProcessed} batches`);
|
|
1322
|
+
this.logger.info(`🔍 Used fields: ${result.fields.slice(0, 10).join(', ')}${result.fields.length > 10 ? '...' : ''}`);
|
|
1323
|
+
// If intelligent selection, log distribution
|
|
1324
|
+
if (intelligent_selection && result.data.length > 0) {
|
|
1325
|
+
const categoryDistribution = {};
|
|
1326
|
+
const priorityDistribution = {};
|
|
1327
|
+
result.data.forEach((inc) => {
|
|
1328
|
+
const category = inc.category || 'uncategorized';
|
|
1329
|
+
const priority = inc.priority || '3';
|
|
1330
|
+
categoryDistribution[category] = (categoryDistribution[category] || 0) + 1;
|
|
1331
|
+
priorityDistribution[priority] = (priorityDistribution[priority] || 0) + 1;
|
|
1332
|
+
});
|
|
1333
|
+
this.logger.info('Data distribution:');
|
|
1334
|
+
this.logger.info(`Categories: ${Object.keys(categoryDistribution).length} unique`);
|
|
1335
|
+
this.logger.info(`Priorities: ${JSON.stringify(priorityDistribution)}`);
|
|
1336
|
+
}
|
|
1337
|
+
// Map the fetched data to our IncidentData format
|
|
1338
|
+
return result.data.map((inc) => ({
|
|
1339
|
+
short_description: inc.short_description || '',
|
|
1340
|
+
description: inc.description || '',
|
|
1341
|
+
category: inc.category || 'uncategorized',
|
|
1342
|
+
subcategory: inc.subcategory || '',
|
|
1343
|
+
priority: parseInt(inc.priority) || 3,
|
|
1344
|
+
impact: parseInt(inc.impact) || 2,
|
|
1345
|
+
urgency: parseInt(inc.urgency) || 2,
|
|
1346
|
+
resolved: inc.state === '6' || inc.state === '7' || inc.active === 'false',
|
|
1347
|
+
resolution_time: inc.resolved_at && inc.sys_created_on ?
|
|
1348
|
+
(new Date(inc.resolved_at).getTime() - new Date(inc.sys_created_on).getTime()) / 1000 : undefined
|
|
1349
|
+
}));
|
|
1350
|
+
}
|
|
1351
|
+
catch (error) {
|
|
1352
|
+
// Fallback to direct fetch with smaller limit if smart fetcher fails
|
|
1353
|
+
this.logger.warn('Smart fetcher failed, using fallback with reduced limit:', error.message);
|
|
1354
|
+
const fallbackLimit = Math.min(limit, 100); // Limit to 100 to avoid token issues
|
|
1355
|
+
const response = await this.client.searchRecords('incident', finalQuery, fallbackLimit);
|
|
1356
|
+
if (!response.success || !response.data?.result) {
|
|
1357
|
+
throw new Error('Failed to fetch incident data. Ensure you have read access to the incident table.');
|
|
1358
|
+
}
|
|
1359
|
+
this.logger.info(`Fetched ${response.data.result.length} incidents (fallback mode, limited to ${fallbackLimit})`);
|
|
1360
|
+
return response.data.result.map((inc) => ({
|
|
1361
|
+
short_description: inc.short_description || '',
|
|
1362
|
+
description: inc.description || '',
|
|
1363
|
+
category: inc.category || 'uncategorized',
|
|
1364
|
+
subcategory: inc.subcategory || '',
|
|
1365
|
+
priority: parseInt(inc.priority) || 3,
|
|
1366
|
+
impact: parseInt(inc.impact) || 2,
|
|
1367
|
+
urgency: parseInt(inc.urgency) || 2,
|
|
1368
|
+
resolved: inc.resolved === 'true',
|
|
1369
|
+
resolution_time: inc.resolved_at && inc.sys_created_on ?
|
|
1370
|
+
(new Date(inc.resolved_at).getTime() - new Date(inc.sys_created_on).getTime()) / 1000 : undefined
|
|
1371
|
+
}));
|
|
1193
1372
|
}
|
|
1194
|
-
return response.data.result.map((inc) => ({
|
|
1195
|
-
short_description: inc.short_description || '',
|
|
1196
|
-
description: inc.description || '',
|
|
1197
|
-
category: inc.category || 'uncategorized',
|
|
1198
|
-
subcategory: inc.subcategory || '',
|
|
1199
|
-
priority: parseInt(inc.priority) || 3,
|
|
1200
|
-
impact: parseInt(inc.impact) || 2,
|
|
1201
|
-
urgency: parseInt(inc.urgency) || 2,
|
|
1202
|
-
resolved: inc.resolved === 'true',
|
|
1203
|
-
resolution_time: inc.resolved_at && inc.sys_created_on ?
|
|
1204
|
-
(new Date(inc.resolved_at).getTime() - new Date(inc.sys_created_on).getTime()) / 1000 : undefined
|
|
1205
|
-
}));
|
|
1206
1373
|
}
|
|
1207
1374
|
async prepareIncidentData(incidents) {
|
|
1208
1375
|
// Create tokenizer
|
|
@@ -1303,28 +1470,111 @@ class ServiceNowMachineLearningMCP {
|
|
|
1303
1470
|
return undefined;
|
|
1304
1471
|
}
|
|
1305
1472
|
async fetchChangeData(limit, includeFailed) {
|
|
1306
|
-
//
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1473
|
+
// Use smart data fetcher for change requests to avoid token limits
|
|
1474
|
+
this.logger.info(`🤖 Using smart ML data fetcher for ${limit} change requests`);
|
|
1475
|
+
const query = includeFailed ? 'state!=cancelled' : 'state=closed^close_code=successful';
|
|
1476
|
+
try {
|
|
1477
|
+
// Create a delegate for the operations MCP
|
|
1478
|
+
const operationsMCP = {
|
|
1479
|
+
handleTool: async (toolName, args) => {
|
|
1480
|
+
if (toolName === 'snow_query_table') {
|
|
1481
|
+
const { table, query: q, limit: l, fields, include_content } = args;
|
|
1482
|
+
// Use the client to fetch data
|
|
1483
|
+
const response = await this.client.searchRecords(table, q || '', l || 100);
|
|
1484
|
+
if (!response.success) {
|
|
1485
|
+
throw new Error(`Query failed: ${response.error}`);
|
|
1486
|
+
}
|
|
1487
|
+
const records = response.data?.result || [];
|
|
1488
|
+
// Filter fields if specified
|
|
1489
|
+
let filteredRecords = records;
|
|
1490
|
+
if (fields && fields.length > 0) {
|
|
1491
|
+
filteredRecords = records.map((record) => {
|
|
1492
|
+
const filtered = {};
|
|
1493
|
+
for (const field of fields) {
|
|
1494
|
+
if (field in record) {
|
|
1495
|
+
filtered[field] = record[field];
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
return filtered;
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
// Format response
|
|
1502
|
+
if (include_content) {
|
|
1503
|
+
return {
|
|
1504
|
+
content: [{
|
|
1505
|
+
type: 'text',
|
|
1506
|
+
text: JSON.stringify(filteredRecords, null, 2)
|
|
1507
|
+
}]
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
else {
|
|
1511
|
+
return {
|
|
1512
|
+
content: [{
|
|
1513
|
+
type: 'text',
|
|
1514
|
+
text: `Found ${records.length} ${table} records matching query: "${q || 'all'}"`
|
|
1515
|
+
}]
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
const dataFetcher = new ml_data_fetcher_js_1.MLDataFetcher(operationsMCP);
|
|
1523
|
+
const result = await dataFetcher.smartFetch({
|
|
1524
|
+
table: 'change_request',
|
|
1525
|
+
query,
|
|
1526
|
+
totalSamples: limit,
|
|
1527
|
+
batchSize: 50, // Small batches to avoid token limits
|
|
1528
|
+
fields: ['number', 'short_description', 'risk', 'impact', 'category', 'type',
|
|
1529
|
+
'state', 'close_code', 'sys_created_on', 'closed_at', 'start_date', 'end_date',
|
|
1530
|
+
'assignment_group', 'approval', 'test_plan', 'backout_plan', 'rollback_tested'],
|
|
1531
|
+
includeContent: true
|
|
1532
|
+
});
|
|
1533
|
+
this.logger.info(`🎉 Fetched ${result.totalFetched} change requests in ${result.batchesProcessed} batches`);
|
|
1534
|
+
return result.data.map((change) => ({
|
|
1535
|
+
short_description: change.short_description || '',
|
|
1536
|
+
risk: change.risk || 'moderate',
|
|
1537
|
+
category: change.category || 'standard',
|
|
1538
|
+
type: change.type || 'standard',
|
|
1539
|
+
planned_start: change.start_date ? new Date(change.start_date) : new Date(),
|
|
1540
|
+
planned_end: change.end_date ? new Date(change.end_date) : new Date(),
|
|
1541
|
+
assignment_group: change.assignment_group?.display_value || change.assignment_group || '',
|
|
1542
|
+
approval_count: parseInt(change.approval) || 0,
|
|
1543
|
+
test_plan: change.test_plan === 'true' || false,
|
|
1544
|
+
backout_plan: change.backout_plan === 'true' || false,
|
|
1545
|
+
rollback_tested: change.rollback_tested === 'true' || false,
|
|
1546
|
+
implementation_success: change.close_code === 'successful'
|
|
1547
|
+
}));
|
|
1548
|
+
}
|
|
1549
|
+
catch (error) {
|
|
1550
|
+
// Fallback to direct API call with smaller limit
|
|
1551
|
+
this.logger.warn('Smart fetcher failed, using fallback:', error.message);
|
|
1552
|
+
const fallbackLimit = Math.min(limit, 100);
|
|
1553
|
+
const queryParams = {
|
|
1554
|
+
sysparm_limit: fallbackLimit,
|
|
1555
|
+
sysparm_query: query,
|
|
1556
|
+
sysparm_fields: 'number,short_description,risk,impact,category,type,state,close_code,sys_created_on,closed_at'
|
|
1557
|
+
};
|
|
1558
|
+
const response = await this.makeServiceNowRequest('/api/now/table/change_request', queryParams);
|
|
1559
|
+
if (!response || !response.result) {
|
|
1560
|
+
throw new Error('Failed to fetch change data from ServiceNow. ' +
|
|
1561
|
+
'Ensure you have permission to read change_request table.');
|
|
1562
|
+
}
|
|
1563
|
+
return response.result.map((change) => ({
|
|
1564
|
+
short_description: change.short_description || '',
|
|
1565
|
+
risk: change.risk || 'moderate',
|
|
1566
|
+
category: change.category || 'standard',
|
|
1567
|
+
type: change.type || 'standard',
|
|
1568
|
+
planned_start: change.start_date ? new Date(change.start_date) : new Date(),
|
|
1569
|
+
planned_end: change.end_date ? new Date(change.end_date) : new Date(),
|
|
1570
|
+
assignment_group: change.assignment_group?.display_value || change.assignment_group || '',
|
|
1571
|
+
approval_count: parseInt(change.approval) || 0,
|
|
1572
|
+
test_plan: change.test_plan === 'true' || false,
|
|
1573
|
+
backout_plan: change.backout_plan === 'true' || false,
|
|
1574
|
+
rollback_tested: change.rollback_tested === 'true' || false,
|
|
1575
|
+
implementation_success: change.close_code === 'successful'
|
|
1576
|
+
}));
|
|
1577
|
+
}
|
|
1328
1578
|
}
|
|
1329
1579
|
async prepareChangeData(changes) {
|
|
1330
1580
|
// Implement change data preparation
|
|
@@ -11,5 +11,75 @@
|
|
|
11
11
|
* - Knowledge base integration
|
|
12
12
|
* - Predictive analytics
|
|
13
13
|
*/
|
|
14
|
-
|
|
14
|
+
declare class ServiceNowOperationsMCP {
|
|
15
|
+
private server;
|
|
16
|
+
private client;
|
|
17
|
+
constructor();
|
|
18
|
+
private setupToolHandlers;
|
|
19
|
+
private handleUniversalQuery;
|
|
20
|
+
private detectCommonFields;
|
|
21
|
+
private handleQueryIncidents;
|
|
22
|
+
private handleAnalyzeIncident;
|
|
23
|
+
private handleAutoResolveIncident;
|
|
24
|
+
private handleQueryRequests;
|
|
25
|
+
private handleQueryProblems;
|
|
26
|
+
private handleCMDBSearch;
|
|
27
|
+
private handleUserLookup;
|
|
28
|
+
private handleOperationalMetrics;
|
|
29
|
+
private handlePatternAnalysis;
|
|
30
|
+
private handleKnowledgeSearch;
|
|
31
|
+
private handlePredictiveAnalysis;
|
|
32
|
+
private processNaturalLanguageQuery;
|
|
33
|
+
private analyzeIncidents;
|
|
34
|
+
private getIncidentDetails;
|
|
35
|
+
private performIncidentAnalysis;
|
|
36
|
+
private generateRootCauseAnalysis;
|
|
37
|
+
private generateAutomatedActions;
|
|
38
|
+
private generateResolutionActions;
|
|
39
|
+
private executeResolutionActions;
|
|
40
|
+
private getRequestItems;
|
|
41
|
+
private getRelatedIncidents;
|
|
42
|
+
private getCIRelationships;
|
|
43
|
+
private getUserDetails;
|
|
44
|
+
private calculateOperationalMetrics;
|
|
45
|
+
private getDateFilter;
|
|
46
|
+
private analyzePatterns;
|
|
47
|
+
private analyzeIncidentPatterns;
|
|
48
|
+
private analyzeRequestTrends;
|
|
49
|
+
private analyzeProblemRootCauses;
|
|
50
|
+
private analyzeUserBehavior;
|
|
51
|
+
private calculateKnowledgeRelevance;
|
|
52
|
+
private performPredictiveAnalysis;
|
|
53
|
+
private analyzeIncidentTrends;
|
|
54
|
+
private analyzeSystemHealthTrends;
|
|
55
|
+
private analyzeResourceTrends;
|
|
56
|
+
private analyzeUserImpactTrends;
|
|
57
|
+
private predictIncidentVolume;
|
|
58
|
+
private predictSystemFailure;
|
|
59
|
+
private predictResourceExhaustion;
|
|
60
|
+
private predictUserImpact;
|
|
61
|
+
private handleCatalogItemManager;
|
|
62
|
+
private createCatalogVariable;
|
|
63
|
+
private mapVariableType;
|
|
64
|
+
private handleCatalogItemSearch;
|
|
65
|
+
private generateSearchVariations;
|
|
66
|
+
private generateSearchSuggestions;
|
|
67
|
+
private shouldExcludeItem;
|
|
68
|
+
private handleCleanupTestArtifacts;
|
|
69
|
+
private cleanupTestCatalogItems;
|
|
70
|
+
private cleanupTestUsers;
|
|
71
|
+
private cleanupTestRequests;
|
|
72
|
+
private getServiceNowUrl;
|
|
73
|
+
private handleCreateUserGroup;
|
|
74
|
+
private handleCreateUser;
|
|
75
|
+
private handleAssignUserToGroup;
|
|
76
|
+
private handleRemoveUserFromGroup;
|
|
77
|
+
private handleListGroupMembers;
|
|
78
|
+
private findUserBySysIdOrUsername;
|
|
79
|
+
private findGroupBySysIdOrName;
|
|
80
|
+
private findDepartment;
|
|
81
|
+
private findLocation;
|
|
82
|
+
run(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
export { ServiceNowOperationsMCP };
|
|
15
85
|
//# sourceMappingURL=servicenow-operations-mcp.d.ts.map
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* - Predictive analytics
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.ServiceNowOperationsMCP = void 0;
|
|
16
17
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
17
18
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
18
19
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
@@ -131,7 +132,7 @@ class ServiceNowOperationsMCP {
|
|
|
131
132
|
// 🎯 UNIVERSAL TABLE QUERY - Works for ANY ServiceNow table!
|
|
132
133
|
{
|
|
133
134
|
name: 'snow_query_table',
|
|
134
|
-
description: '🚀 Universal high-performance query tool for ANY ServiceNow table - optimized for memory efficiency',
|
|
135
|
+
description: '🚀 Universal high-performance query tool for ANY ServiceNow table - optimized for memory efficiency. SMART DEFAULTS: 1000 records (5000 for ML training contexts)',
|
|
135
136
|
inputSchema: {
|
|
136
137
|
type: 'object',
|
|
137
138
|
properties: {
|
|
@@ -146,8 +147,8 @@ class ServiceNowOperationsMCP {
|
|
|
146
147
|
},
|
|
147
148
|
limit: {
|
|
148
149
|
type: 'number',
|
|
149
|
-
description: 'Maximum number of results
|
|
150
|
-
default:
|
|
150
|
+
description: '🎯 Maximum number of results. SMART DEFAULTS: 1000 (normal queries), 5000 (ML training contexts). Set higher for large ML datasets (10000+)',
|
|
151
|
+
default: 1000
|
|
151
152
|
},
|
|
152
153
|
include_content: {
|
|
153
154
|
type: 'boolean',
|
|
@@ -828,8 +829,37 @@ class ServiceNowOperationsMCP {
|
|
|
828
829
|
});
|
|
829
830
|
}
|
|
830
831
|
async handleUniversalQuery(args) {
|
|
831
|
-
|
|
832
|
-
|
|
832
|
+
// 🎯 SMART DEFAULT LIMITS - Context-aware for ML training
|
|
833
|
+
const determineSmartLimit = (providedLimit, table, query, includeContent) => {
|
|
834
|
+
if (providedLimit !== undefined)
|
|
835
|
+
return providedLimit; // User explicitly set limit
|
|
836
|
+
// ML Training context detection
|
|
837
|
+
const isMLContext = query?.toLowerCase().includes('train') ||
|
|
838
|
+
query?.toLowerCase().includes('ml') ||
|
|
839
|
+
table?.toLowerCase().includes('train') ||
|
|
840
|
+
(includeContent && table === 'incident'); // ML often needs incident content
|
|
841
|
+
if (isMLContext) {
|
|
842
|
+
logger_js_1.logger.info(`🧠 ML context detected - using ML-optimized limit: 5000`);
|
|
843
|
+
return 5000; // ML training needs more data
|
|
844
|
+
}
|
|
845
|
+
// Count-only queries can handle more records efficiently
|
|
846
|
+
if (!includeContent) {
|
|
847
|
+
return 2000; // Count queries are memory-efficient
|
|
848
|
+
}
|
|
849
|
+
// Normal content queries
|
|
850
|
+
return 1000; // Balanced default for content queries
|
|
851
|
+
};
|
|
852
|
+
const { table, query, include_content = false, fields, include_display_values = false, group_by, order_by } = args;
|
|
853
|
+
// Apply smart limit logic
|
|
854
|
+
const limit = determineSmartLimit(args.limit, table, query, include_content || !!fields);
|
|
855
|
+
// 🚨 ML Training Warning for low limits
|
|
856
|
+
const isMLTrainingContext = query?.toLowerCase().includes('train') ||
|
|
857
|
+
query?.toLowerCase().includes('ml') ||
|
|
858
|
+
args.limit !== undefined && args.limit < 1000;
|
|
859
|
+
if (isMLTrainingContext && limit < 1000) {
|
|
860
|
+
logger_js_1.logger.warn(`⚠️ ML Training detected with low limit (${limit}). Consider setting limit=5000+ for better training data!`);
|
|
861
|
+
}
|
|
862
|
+
logger_js_1.logger.info(`Universal query on table '${table}' with: ${query} (limit: ${limit}, include_content: ${include_content})`);
|
|
833
863
|
try {
|
|
834
864
|
// Convert natural language to ServiceNow query if needed
|
|
835
865
|
const processedQuery = this.processNaturalLanguageQuery(query, table);
|
|
@@ -3140,10 +3170,13 @@ class ServiceNowOperationsMCP {
|
|
|
3140
3170
|
logger_js_1.logger.info('ServiceNow Operations MCP Server started');
|
|
3141
3171
|
}
|
|
3142
3172
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3173
|
+
exports.ServiceNowOperationsMCP = ServiceNowOperationsMCP;
|
|
3174
|
+
// Start the server only if run directly
|
|
3175
|
+
if (require.main === module) {
|
|
3176
|
+
const server = new ServiceNowOperationsMCP();
|
|
3177
|
+
server.run().catch((error) => {
|
|
3178
|
+
logger_js_1.logger.error('Failed to start ServiceNow Operations MCP server:', error);
|
|
3179
|
+
process.exit(1);
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3149
3182
|
//# sourceMappingURL=servicenow-operations-mcp.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Test Smart Limits for snow_query_table
|
|
4
|
+
*
|
|
5
|
+
* This script validates that the smart default limit system works correctly:
|
|
6
|
+
* - ML context: 5000 default
|
|
7
|
+
* - Count-only: 2000 default
|
|
8
|
+
* - Normal content: 1000 default
|
|
9
|
+
* - Explicit limits: Always respected
|
|
10
|
+
*/
|
|
11
|
+
declare function testSmartLimits(): Promise<void>;
|
|
12
|
+
export { testSmartLimits };
|
|
13
|
+
//# sourceMappingURL=test-smart-limits.d.ts.map
|