snow-flow 3.0.10 → 3.0.14

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.
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Intelligent ServiceNow Reporting MCP Server
4
+ * Uses snow_query_table for real table discovery instead of hardcoded mappings
5
+ */
6
+ export {};
7
+ //# sourceMappingURL=intelligent-reporting-mcp.d.ts.map
@@ -0,0 +1,379 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Intelligent ServiceNow Reporting MCP Server
5
+ * Uses snow_query_table for real table discovery instead of hardcoded mappings
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
9
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
10
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
11
+ const servicenow_client_js_1 = require("../utils/servicenow-client.js");
12
+ const mcp_auth_middleware_js_1 = require("../utils/mcp-auth-middleware.js");
13
+ const logger_js_1 = require("../utils/logger.js");
14
+ const anti_mock_data_validator_js_1 = require("../utils/anti-mock-data-validator.js");
15
+ class IntelligentReportingMCP {
16
+ constructor() {
17
+ this.server = new index_js_1.Server({
18
+ name: 'intelligent-reporting',
19
+ version: '1.0.0',
20
+ }, {
21
+ capabilities: {
22
+ tools: {},
23
+ },
24
+ });
25
+ this.client = new servicenow_client_js_1.ServiceNowClient();
26
+ this.logger = new logger_js_1.Logger('IntelligentReportingMCP');
27
+ this.setupHandlers();
28
+ }
29
+ setupHandlers() {
30
+ this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
31
+ tools: [
32
+ {
33
+ name: 'snow_intelligent_report',
34
+ description: '🔥 REAL DATA ONLY: Creates reports with intelligent table discovery using LIVE ServiceNow data. Input any description and it finds the right table with REAL data from your instance. NO mock/demo data used.',
35
+ inputSchema: {
36
+ type: 'object',
37
+ properties: {
38
+ name: { type: 'string', description: 'Report name (e.g., "ITSM Trend Analysis")' },
39
+ description: { type: 'string', description: 'User description of what they want (e.g., "ITSM Overview Metrics", "Change Request Pipeline")' },
40
+ conditions: { type: 'string', description: 'Optional filter conditions' },
41
+ format: { type: 'string', description: 'Output format (PDF, Excel, CSV)' },
42
+ includeAnalysis: { type: 'boolean', description: 'Include data analysis and insights' }
43
+ },
44
+ required: ['name', 'description']
45
+ }
46
+ },
47
+ {
48
+ name: 'snow_intelligent_dashboard',
49
+ description: '🔥 REAL DATA ONLY: Creates dashboards with intelligent discovery using LIVE ServiceNow data. Automatically finds relevant data from your actual instance. NO mock/demo data used.',
50
+ inputSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ name: { type: 'string', description: 'Dashboard name' },
54
+ description: { type: 'string', description: 'What the user wants to see (e.g., "Operations overview", "Service desk metrics")' },
55
+ refreshInterval: { type: 'number', description: 'Refresh interval in minutes' }
56
+ },
57
+ required: ['name', 'description']
58
+ }
59
+ }
60
+ ]
61
+ }));
62
+ this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
63
+ try {
64
+ const { name, arguments: args } = request.params;
65
+ const authResult = await mcp_auth_middleware_js_1.mcpAuth.ensureAuthenticated();
66
+ if (!authResult.success) {
67
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, authResult.error || 'Authentication required');
68
+ }
69
+ switch (name) {
70
+ case 'snow_intelligent_report':
71
+ return await this.createIntelligentReport(args);
72
+ case 'snow_intelligent_dashboard':
73
+ return await this.createIntelligentDashboard(args);
74
+ default:
75
+ throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
76
+ }
77
+ }
78
+ catch (error) {
79
+ this.logger.error(`Error in ${request.params.name}:`, error);
80
+ throw error;
81
+ }
82
+ });
83
+ }
84
+ /**
85
+ * Create report with intelligent table discovery
86
+ */
87
+ async createIntelligentReport(args) {
88
+ try {
89
+ this.logger.info(`🔍 Creating intelligent report for: "${args.description}"`);
90
+ // Step 1: Discover the right table(s) based on user description
91
+ const discoveredTables = await this.discoverRelevantTables(args.description);
92
+ if (discoveredTables.length === 0) {
93
+ throw new Error(`Could not find relevant ServiceNow tables for: "${args.description}". Try being more specific, like: "incident metrics", "change requests", or "user statistics".`);
94
+ }
95
+ // Step 2: Use the best matching table
96
+ const primaryTable = discoveredTables[0];
97
+ this.logger.info(`✅ Using table: ${primaryTable.name} (${primaryTable.recordCount} records)`);
98
+ // Step 3: Get real data sample to understand the structure
99
+ await this.enrichTableWithSampleData(primaryTable);
100
+ // Step 4: Create the actual report
101
+ const reportData = {
102
+ title: args.name,
103
+ table: primaryTable.name,
104
+ description: `${args.description} - Auto-discovered from ${primaryTable.label}`,
105
+ filter: args.conditions || this.buildIntelligentFilter(args.description, primaryTable),
106
+ field_list: this.selectRelevantFields(primaryTable, args.description),
107
+ type: this.determineReportType(args.description),
108
+ is_published: true,
109
+ active: true
110
+ };
111
+ const response = await this.client.createRecord('sys_report', reportData);
112
+ if (!response.success) {
113
+ throw new Error(`Failed to create report: ${response.error}`);
114
+ }
115
+ // Step 5: Generate insights if requested
116
+ let insights = '';
117
+ if (args.includeAnalysis) {
118
+ insights = await this.generateDataInsights(primaryTable);
119
+ }
120
+ const reportUrl = `${process.env.SNOW_INSTANCE}/sys_report_template.do?jvar_report_id=${response.data.sys_id}`;
121
+ return {
122
+ content: [{
123
+ type: 'text',
124
+ text: `✅ Intelligent Report Created!
125
+
126
+ 📊 **${args.name}**
127
+ 🆔 sys_id: ${response.data.sys_id}
128
+
129
+ 🔍 **Discovery Results:**
130
+ 📋 Found Table: ${primaryTable.label} (${primaryTable.name})
131
+ 📈 Records Available: ${primaryTable.recordCount}
132
+ 📝 Fields Used: ${reportData.field_list}
133
+
134
+ 🎯 **Report Details:**
135
+ ${args.conditions ? `🔍 Custom Filter: ${args.conditions}` : `🤖 Smart Filter: ${reportData.filter}`}
136
+ 📄 Format: ${args.format || 'HTML'}
137
+ 🔗 View Report: ${reportUrl}
138
+
139
+ ${insights ? `📊 **Data Insights:**
140
+ ${insights}
141
+
142
+ ` : ''}🚀 Report ready with real ServiceNow data!`
143
+ }]
144
+ };
145
+ }
146
+ catch (error) {
147
+ this.logger.error('Failed to create intelligent report:', error);
148
+ throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create intelligent report: ${error}`);
149
+ }
150
+ }
151
+ /**
152
+ * Discover relevant tables using snow_query_table
153
+ */
154
+ async discoverRelevantTables(description) {
155
+ try {
156
+ const results = [];
157
+ // Extract keywords from description
158
+ const keywords = this.extractKeywords(description);
159
+ this.logger.info(`🔑 Keywords: ${keywords.join(', ')}`);
160
+ // Search for tables that might contain relevant data
161
+ const candidateTables = await this.findCandidateTables(keywords);
162
+ // Test each candidate table to see if it has data
163
+ for (const candidate of candidateTables) {
164
+ try {
165
+ // Use snow_query_table MCP to test the table
166
+ const testQuery = await this.testTableWithQuery(candidate, keywords);
167
+ if (testQuery && testQuery.recordCount > 0) {
168
+ results.push({
169
+ name: candidate,
170
+ label: testQuery.label || candidate,
171
+ recordCount: testQuery.recordCount,
172
+ fields: testQuery.fields || []
173
+ });
174
+ }
175
+ }
176
+ catch (error) {
177
+ this.logger.warn(`Table ${candidate} test failed:`, error);
178
+ }
179
+ }
180
+ // Sort by relevance (record count and keyword matches)
181
+ return results.sort((a, b) => b.recordCount - a.recordCount);
182
+ }
183
+ catch (error) {
184
+ this.logger.error('Table discovery failed:', error);
185
+ return [];
186
+ }
187
+ }
188
+ /**
189
+ * Extract meaningful keywords from user description
190
+ */
191
+ extractKeywords(description) {
192
+ const text = description.toLowerCase();
193
+ // ServiceNow domain keywords
194
+ const domainKeywords = [
195
+ 'incident', 'problem', 'change', 'request', 'task', 'user', 'asset', 'configuration',
196
+ 'service', 'catalog', 'knowledge', 'article', 'approval', 'workflow', 'sla',
197
+ 'metric', 'kpi', 'analytics', 'report', 'dashboard', 'overview', 'trend', 'analysis'
198
+ ];
199
+ const found = domainKeywords.filter(keyword => text.includes(keyword));
200
+ // Add any other significant words (3+ chars, not common words)
201
+ const words = text.split(/\s+/).filter(word => word.length >= 3 &&
202
+ !['the', 'and', 'for', 'with', 'from', 'that', 'this', 'are', 'was'].includes(word));
203
+ return [...new Set([...found, ...words])];
204
+ }
205
+ /**
206
+ * Find candidate tables based on keywords
207
+ */
208
+ async findCandidateTables(keywords) {
209
+ const candidates = new Set();
210
+ // Keyword-to-table mapping based on ServiceNow knowledge
211
+ const tableMapping = {
212
+ 'incident': ['incident'],
213
+ 'problem': ['problem'],
214
+ 'change': ['change_request'],
215
+ 'request': ['sc_request', 'sc_req_item', 'change_request'],
216
+ 'task': ['task', 'sc_task'],
217
+ 'user': ['sys_user'],
218
+ 'asset': ['alm_asset'],
219
+ 'configuration': ['cmdb_ci'],
220
+ 'service': ['service_offering', 'sc_cat_item'],
221
+ 'catalog': ['sc_cat_item', 'sc_category'],
222
+ 'knowledge': ['kb_knowledge'],
223
+ 'approval': ['sysapproval_approver'],
224
+ 'workflow': ['wf_workflow'],
225
+ 'metric': ['sys_report', 'pa_dashboards'],
226
+ 'kpi': ['pa_indicators'],
227
+ 'overview': ['incident', 'change_request', 'problem', 'task'],
228
+ 'trend': ['incident', 'change_request', 'problem'],
229
+ 'analysis': ['incident', 'change_request', 'problem', 'task']
230
+ };
231
+ // Add tables based on keyword matches
232
+ for (const keyword of keywords) {
233
+ if (tableMapping[keyword]) {
234
+ tableMapping[keyword].forEach(table => candidates.add(table));
235
+ }
236
+ }
237
+ // Default fallback tables if no specific matches
238
+ if (candidates.size === 0) {
239
+ ['incident', 'change_request', 'problem', 'task', 'sys_user'].forEach(table => candidates.add(table));
240
+ }
241
+ return Array.from(candidates);
242
+ }
243
+ /**
244
+ * Test a table using actual ServiceNow query
245
+ */
246
+ async testTableWithQuery(tableName, keywords) {
247
+ try {
248
+ // First, get table info
249
+ const tableInfo = await this.client.searchRecords('sys_db_object', `name=${tableName}`, 1);
250
+ const label = tableInfo.success && tableInfo.data?.result?.length > 0
251
+ ? tableInfo.data.result[0].label
252
+ : tableName;
253
+ // Get record count and sample fields
254
+ const query = await this.client.searchRecords(tableName, '', 20); // Real data sample (increased for better analysis)
255
+ if (query.success && query.data?.result) {
256
+ const records = query.data.result;
257
+ // 🔥 ENFORCE ZERO MOCK DATA TOLERANCE
258
+ (0, anti_mock_data_validator_js_1.validateRealData)(records, `Table Discovery for ${tableName}`);
259
+ const fields = records.length > 0 ? Object.keys(records[0]) : [];
260
+ // Get total count (this is a bit hacky, but ServiceNow doesn't have a direct count API)
261
+ const countQuery = await this.client.searchRecords(tableName, '', 1);
262
+ const recordCount = countQuery.success ? (countQuery.data?.result?.length > 0 ? 100 : 0) : 0; // Estimate
263
+ return { recordCount, label, fields };
264
+ }
265
+ return null;
266
+ }
267
+ catch (error) {
268
+ this.logger.error(`Failed to test table ${tableName}:`, error);
269
+ return null;
270
+ }
271
+ }
272
+ /**
273
+ * Enrich table data with sample records
274
+ */
275
+ async enrichTableWithSampleData(table) {
276
+ try {
277
+ const sampleQuery = await this.client.searchRecords(table.name, '', 10); // Real data sample (increased)
278
+ if (sampleQuery.success && sampleQuery.data?.result) {
279
+ // 🔥 ENFORCE ZERO MOCK DATA TOLERANCE
280
+ (0, anti_mock_data_validator_js_1.validateRealData)(sampleQuery.data.result, `Sample Data for ${table.name}`);
281
+ table.sampleData = sampleQuery.data.result;
282
+ }
283
+ }
284
+ catch (error) {
285
+ this.logger.warn(`Could not get sample data for ${table.name}:`, error);
286
+ }
287
+ }
288
+ /**
289
+ * Build intelligent filter based on description
290
+ */
291
+ buildIntelligentFilter(description, table) {
292
+ const desc = description.toLowerCase();
293
+ // Common filters based on description patterns
294
+ if (desc.includes('active') || desc.includes('current') || desc.includes('open')) {
295
+ if (table.fields.includes('active'))
296
+ return 'active=true';
297
+ if (table.fields.includes('state') && table.name === 'incident')
298
+ return 'stateNOT IN6,7,8';
299
+ }
300
+ if (desc.includes('recent') || desc.includes('last month') || desc.includes('30 days')) {
301
+ return 'sys_created_on>=javascript:gs.daysAgoStart(30)';
302
+ }
303
+ if (desc.includes('high priority') || desc.includes('critical')) {
304
+ return 'priority<=2';
305
+ }
306
+ return ''; // No filter
307
+ }
308
+ /**
309
+ * Select relevant fields for the report
310
+ */
311
+ selectRelevantFields(table, description) {
312
+ const desc = description.toLowerCase();
313
+ // Smart field selection based on table type
314
+ const fieldSets = {
315
+ 'incident': ['number', 'short_description', 'priority', 'state', 'assigned_to', 'sys_created_on'],
316
+ 'change_request': ['number', 'short_description', 'type', 'state', 'requested_by', 'sys_created_on'],
317
+ 'problem': ['number', 'short_description', 'priority', 'state', 'assigned_to', 'root_cause'],
318
+ 'sys_user': ['name', 'user_name', 'email', 'department', 'title', 'active'],
319
+ 'task': ['number', 'short_description', 'priority', 'state', 'assigned_to', 'sys_created_on']
320
+ };
321
+ const defaultFields = fieldSets[table.name] || ['sys_id', 'sys_created_on', 'sys_updated_on'];
322
+ // Filter to only include fields that actually exist
323
+ const availableFields = defaultFields.filter(field => table.fields.includes(field));
324
+ return availableFields.length > 0 ? availableFields.join(',') : table.fields.slice(0, 6).join(',');
325
+ }
326
+ /**
327
+ * Determine report type based on description
328
+ */
329
+ determineReportType(description) {
330
+ const desc = description.toLowerCase();
331
+ if (desc.includes('trend') || desc.includes('over time'))
332
+ return 'trend';
333
+ if (desc.includes('count') || desc.includes('summary'))
334
+ return 'list';
335
+ if (desc.includes('chart') || desc.includes('graph'))
336
+ return 'bar';
337
+ return 'list'; // Default
338
+ }
339
+ /**
340
+ * Generate data insights
341
+ */
342
+ async generateDataInsights(table) {
343
+ if (!table.sampleData || table.sampleData.length === 0) {
344
+ return 'No sample data available for insights';
345
+ }
346
+ const insights = [];
347
+ insights.push(`Sample shows ${table.sampleData.length} records from ${table.label}`);
348
+ // Analyze common patterns
349
+ const sample = table.sampleData[0];
350
+ if (sample.state)
351
+ insights.push(`Records have state tracking`);
352
+ if (sample.priority)
353
+ insights.push(`Priority levels available`);
354
+ if (sample.assigned_to)
355
+ insights.push(`Assignment tracking enabled`);
356
+ return insights.join('\n');
357
+ }
358
+ /**
359
+ * Create intelligent dashboard
360
+ */
361
+ async createIntelligentDashboard(args) {
362
+ // Similar intelligent discovery logic for dashboards
363
+ // This would use the same table discovery but create dashboard widgets instead
364
+ return {
365
+ content: [{
366
+ type: 'text',
367
+ text: `Intelligent Dashboard creation coming soon! For now, use snow_intelligent_report to create reports with intelligent table discovery.`
368
+ }]
369
+ };
370
+ }
371
+ async run() {
372
+ const transport = new stdio_js_1.StdioServerTransport();
373
+ await this.server.connect(transport);
374
+ this.logger.info('Intelligent ServiceNow Reporting MCP Server running on stdio');
375
+ }
376
+ }
377
+ const server = new IntelligentReportingMCP();
378
+ server.run().catch(console.error);
379
+ //# sourceMappingURL=intelligent-reporting-mcp.js.map
@@ -1835,7 +1835,7 @@ ${args.widgets && args.widgets.length > 0 ? args.widgets.map((w, i) => `
1835
1835
  switch (flowType) {
1836
1836
  case 'flow':
1837
1837
  // Create workflow record in ServiceNow
1838
- result = await this.client.create('wf_workflow', {
1838
+ result = await this.client.createRecord('wf_workflow', {
1839
1839
  name: flowData.name || `flow_${Date.now()}`,
1840
1840
  description: flowData.description || 'Created by Snow-Flow',
1841
1841
  table: flowData.table || 'incident',
@@ -1847,7 +1847,7 @@ ${args.widgets && args.widgets.length > 0 ? args.widgets.map((w, i) => `
1847
1847
  break;
1848
1848
  case 'subflow':
1849
1849
  // Create subflow as a workflow activity
1850
- result = await this.client.create('wf_workflow', {
1850
+ result = await this.client.createRecord('wf_workflow', {
1851
1851
  name: flowData.name || `subflow_${Date.now()}`,
1852
1852
  description: `${flowData.description || 'Subflow created by Snow-Flow'} [SUBFLOW]`,
1853
1853
  table: flowData.table || 'incident',
@@ -1862,7 +1862,7 @@ ${args.widgets && args.widgets.length > 0 ? args.widgets.map((w, i) => `
1862
1862
  if (!flowData.workflow_id) {
1863
1863
  throw new Error('workflow_id is required for flow actions');
1864
1864
  }
1865
- result = await this.client.create('wf_activity', {
1865
+ result = await this.client.createRecord('wf_activity', {
1866
1866
  workflow: flowData.workflow_id,
1867
1867
  name: flowData.name || `action_${Date.now()}`,
1868
1868
  script: flowData.script || '// Action script here',
@@ -2627,7 +2627,7 @@ ${hasErrors ? '❌ Validation failed - fix errors before deployment' : '✅ Vali
2627
2627
  let rollbackResult;
2628
2628
  try {
2629
2629
  // Set update set to ignore state (ServiceNow's way of "rolling back")
2630
- rollbackResult = await this.client.update(`sys_update_set/${update_set_id}`, {
2630
+ rollbackResult = await this.client.updateRecord(`sys_update_set/${update_set_id}`, {
2631
2631
  state: 'ignore',
2632
2632
  description: `${updateSet.description || ''} - ROLLED BACK: ${reason}`
2633
2633
  });
@@ -3067,12 +3067,12 @@ ${deploymentList || 'No recent deployments found in the last 7 days'}
3067
3067
  if (existingArtifact) {
3068
3068
  // Update existing artifact
3069
3069
  const { sys_id, ...updateData } = artifact;
3070
- result = await this.client.update(`${tableName}/${sys_id}`, updateData);
3070
+ result = await this.client.updateRecord(`${tableName}/${sys_id}`, updateData);
3071
3071
  action = 'updated';
3072
3072
  }
3073
3073
  else {
3074
3074
  // Create new artifact
3075
- result = await this.client.create(tableName, artifact);
3075
+ result = await this.client.createRecord(tableName, artifact);
3076
3076
  action = 'created';
3077
3077
  }
3078
3078
  if (!result?.result) {
@@ -3835,25 +3835,39 @@ This will use your .env credentials to start the OAuth flow and generate access
3835
3835
  template: '<div>Test widget - safe to delete</div>',
3836
3836
  description: 'Temporary test widget created by Snow-Flow MCP diagnostics'
3837
3837
  };
3838
- const createResult = await this.client.create('sp_widget', testWidget);
3839
- if (createResult?.result?.sys_id) {
3838
+ const createResult = await this.client.createRecord('sp_widget', testWidget);
3839
+ if (createResult?.success && createResult?.data?.sys_id) {
3840
3840
  // Immediately delete the test widget
3841
3841
  try {
3842
- await this.client.delete(`sp_widget/${createResult.result.sys_id}`);
3842
+ await this.client.deleteRecord('sp_widget', createResult.data.sys_id);
3843
3843
  realApiTests.writePermissions = {
3844
3844
  status: '✅ Full Access',
3845
3845
  description: 'Can create and delete artifacts - full deployment capability',
3846
- details: `Successfully created and cleaned up test widget ${createResult.result.sys_id}`
3846
+ details: `Successfully created and cleaned up test widget ${createResult.data.sys_id}`
3847
3847
  };
3848
3848
  }
3849
3849
  catch (deleteError) {
3850
3850
  realApiTests.writePermissions = {
3851
3851
  status: '⚠️ Partial',
3852
3852
  description: 'Can create but cannot delete - cleanup may be needed',
3853
- details: `Created test widget ${createResult.result.sys_id} but failed to delete: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
3853
+ details: `Created test widget ${createResult.data.sys_id} but failed to delete: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`
3854
3854
  };
3855
3855
  }
3856
3856
  }
3857
+ else {
3858
+ // Log what we got for debugging
3859
+ this.logger.warn('Create succeeded but unexpected response structure:', {
3860
+ success: createResult?.success,
3861
+ hasData: !!createResult?.data,
3862
+ hasSysId: !!createResult?.data?.sys_id,
3863
+ dataKeys: createResult?.data ? Object.keys(createResult.data) : []
3864
+ });
3865
+ realApiTests.writePermissions = {
3866
+ status: '⚠️ Partial',
3867
+ description: 'Created widget but response structure unexpected',
3868
+ details: `Response structure: ${JSON.stringify(createResult?.data || {})}`
3869
+ };
3870
+ }
3857
3871
  }
3858
3872
  catch (writeError) {
3859
3873
  realApiTests.writePermissions = {