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,428 @@
1
+ /**
2
+ * ServiceNow MCP Server - Script Synchronization
3
+ *
4
+ * Copyright (c) 2025 Happy Technologies LLC
5
+ * Licensed under the MIT License - see LICENSE file for details
6
+ *
7
+ * Enables local script development with Git integration.
8
+ * Supports bidirectional sync between local files and ServiceNow.
9
+ *
10
+ * Features:
11
+ * - Single script sync (push/pull)
12
+ * - Bulk sync (all scripts in directory)
13
+ * - Watch mode (auto-sync on file changes)
14
+ * - Git-friendly file naming convention
15
+ */
16
+
17
+ import fs from 'fs/promises';
18
+ import path from 'path';
19
+ import chokidar from 'chokidar';
20
+
21
+ /**
22
+ * Supported script types with their ServiceNow table mappings
23
+ */
24
+ export const SCRIPT_TYPES = {
25
+ sys_script_include: {
26
+ table: 'sys_script_include',
27
+ label: 'Script Include',
28
+ name_field: 'name',
29
+ script_field: 'script',
30
+ extension: '.js'
31
+ },
32
+ sys_script: {
33
+ table: 'sys_script',
34
+ label: 'Business Rule',
35
+ name_field: 'name',
36
+ script_field: 'script',
37
+ extension: '.js'
38
+ },
39
+ sys_ui_script: {
40
+ table: 'sys_ui_script',
41
+ label: 'UI Script',
42
+ name_field: 'name',
43
+ script_field: 'script',
44
+ extension: '.js'
45
+ },
46
+ sys_ui_action: {
47
+ table: 'sys_ui_action',
48
+ label: 'UI Action',
49
+ name_field: 'name',
50
+ script_field: 'script',
51
+ extension: '.js'
52
+ },
53
+ sys_script_client: {
54
+ table: 'sys_script_client',
55
+ label: 'Client Script',
56
+ name_field: 'name',
57
+ script_field: 'script',
58
+ extension: '.js'
59
+ }
60
+ };
61
+
62
+ /**
63
+ * Parse file name to extract script name and type
64
+ * Format: {script_name}.{script_type}.js
65
+ *
66
+ * @param {string} fileName - File name to parse
67
+ * @returns {object} - { scriptName, scriptType, isValid }
68
+ */
69
+ export function parseFileName(fileName) {
70
+ const parts = fileName.split('.');
71
+
72
+ // Must have at least 3 parts: name, type, js
73
+ if (parts.length < 3) {
74
+ return { isValid: false };
75
+ }
76
+
77
+ const extension = parts.pop(); // Remove .js
78
+ const scriptType = parts.pop(); // Remove script type
79
+ const scriptName = parts.join('.'); // Rest is the name
80
+
81
+ if (extension !== 'js' || !SCRIPT_TYPES[scriptType]) {
82
+ return { isValid: false };
83
+ }
84
+
85
+ return {
86
+ isValid: true,
87
+ scriptName,
88
+ scriptType
89
+ };
90
+ }
91
+
92
+ /**
93
+ * Generate file name from script name and type
94
+ *
95
+ * @param {string} scriptName - Script name
96
+ * @param {string} scriptType - Script type
97
+ * @returns {string} - File name
98
+ */
99
+ export function generateFileName(scriptName, scriptType) {
100
+ const sanitizedName = scriptName.replace(/[^a-zA-Z0-9_.-]/g, '_');
101
+ return `${sanitizedName}.${scriptType}.js`;
102
+ }
103
+
104
+ /**
105
+ * Sync a single script between local file and ServiceNow
106
+ *
107
+ * @param {object} serviceNowClient - ServiceNow client instance
108
+ * @param {object} options - Sync options
109
+ * @param {string} options.script_name - Name of the script in ServiceNow
110
+ * @param {string} options.script_type - Type of script (sys_script_include, etc.)
111
+ * @param {string} options.file_path - Local file path
112
+ * @param {string} options.direction - 'push' or 'pull' (auto-detect if not specified)
113
+ * @param {string} options.instance - ServiceNow instance name (optional)
114
+ * @returns {object} - Sync result
115
+ */
116
+ export async function syncScript(serviceNowClient, options) {
117
+ const { script_name, script_type, file_path, direction, instance } = options;
118
+
119
+ // Validate script type
120
+ const scriptConfig = SCRIPT_TYPES[script_type];
121
+ if (!scriptConfig) {
122
+ throw new Error(`Invalid script type: ${script_type}. Supported types: ${Object.keys(SCRIPT_TYPES).join(', ')}`);
123
+ }
124
+
125
+ const result = {
126
+ script_name,
127
+ script_type,
128
+ file_path,
129
+ direction: null,
130
+ success: false,
131
+ timestamp: new Date().toISOString(),
132
+ error: null
133
+ };
134
+
135
+ try {
136
+ // Auto-detect direction if not specified
137
+ let syncDirection = direction;
138
+ if (!syncDirection) {
139
+ try {
140
+ await fs.access(file_path);
141
+ syncDirection = 'push'; // File exists, push to ServiceNow
142
+ } catch {
143
+ syncDirection = 'pull'; // File doesn't exist, pull from ServiceNow
144
+ }
145
+ }
146
+
147
+ result.direction = syncDirection;
148
+
149
+ if (syncDirection === 'pull') {
150
+ // Pull from ServiceNow to local file
151
+ const records = await serviceNowClient.getRecords(scriptConfig.table, {
152
+ sysparm_query: `${scriptConfig.name_field}=${script_name}`,
153
+ sysparm_limit: 1,
154
+ sysparm_fields: `sys_id,${scriptConfig.name_field},${scriptConfig.script_field}`
155
+ });
156
+
157
+ if (records.length === 0) {
158
+ throw new Error(`Script not found in ServiceNow: ${script_name}`);
159
+ }
160
+
161
+ const record = records[0];
162
+ const scriptContent = record[scriptConfig.script_field] || '';
163
+
164
+ // Add metadata header
165
+ const fileContent = `/**
166
+ * ServiceNow Script: ${record[scriptConfig.name_field]}
167
+ * Type: ${scriptConfig.label}
168
+ * Table: ${scriptConfig.table}
169
+ * sys_id: ${record.sys_id}
170
+ *
171
+ * Last synced: ${new Date().toISOString()}
172
+ *
173
+ * This file is managed by ServiceNow MCP Script Sync.
174
+ * Changes will be pushed to ServiceNow on save.
175
+ */
176
+
177
+ ${scriptContent}`;
178
+
179
+ // Ensure directory exists
180
+ const dir = path.dirname(file_path);
181
+ await fs.mkdir(dir, { recursive: true });
182
+
183
+ // Write file
184
+ await fs.writeFile(file_path, fileContent, 'utf-8');
185
+
186
+ result.success = true;
187
+ result.sys_id = record.sys_id;
188
+ result.message = `Successfully pulled script from ServiceNow to ${file_path}`;
189
+
190
+ } else if (syncDirection === 'push') {
191
+ // Push from local file to ServiceNow
192
+ let fileContent;
193
+ try {
194
+ fileContent = await fs.readFile(file_path, 'utf-8');
195
+ } catch (error) {
196
+ throw new Error(`Failed to read file: ${error.message}`);
197
+ }
198
+
199
+ // Remove metadata header if present (lines starting with /** to */)
200
+ let scriptContent = fileContent;
201
+ const headerMatch = fileContent.match(/^\/\*\*[\s\S]*?\*\//);
202
+ if (headerMatch) {
203
+ scriptContent = fileContent.substring(headerMatch[0].length).trim();
204
+ }
205
+
206
+ // Find existing script in ServiceNow
207
+ const records = await serviceNowClient.getRecords(scriptConfig.table, {
208
+ sysparm_query: `${scriptConfig.name_field}=${script_name}`,
209
+ sysparm_limit: 1,
210
+ sysparm_fields: `sys_id,${scriptConfig.name_field}`
211
+ });
212
+
213
+ if (records.length === 0) {
214
+ throw new Error(`Script not found in ServiceNow: ${script_name}. Create it first, then sync.`);
215
+ }
216
+
217
+ const record = records[0];
218
+
219
+ // Update script in ServiceNow
220
+ await serviceNowClient.updateRecord(scriptConfig.table, record.sys_id, {
221
+ [scriptConfig.script_field]: scriptContent
222
+ });
223
+
224
+ result.success = true;
225
+ result.sys_id = record.sys_id;
226
+ result.message = `Successfully pushed script from ${file_path} to ServiceNow`;
227
+
228
+ } else {
229
+ throw new Error(`Invalid direction: ${syncDirection}. Must be 'push' or 'pull'.`);
230
+ }
231
+
232
+ } catch (error) {
233
+ result.error = error.message;
234
+ result.message = `Sync failed: ${error.message}`;
235
+ }
236
+
237
+ return result;
238
+ }
239
+
240
+ /**
241
+ * Sync all scripts in a directory
242
+ *
243
+ * @param {object} serviceNowClient - ServiceNow client instance
244
+ * @param {object} options - Sync options
245
+ * @param {string} options.directory - Directory containing scripts
246
+ * @param {array} options.script_types - Script types to sync (optional, defaults to all)
247
+ * @param {string} options.instance - ServiceNow instance name (optional)
248
+ * @returns {object} - Sync results
249
+ */
250
+ export async function syncAllScripts(serviceNowClient, options) {
251
+ const { directory, script_types, instance } = options;
252
+
253
+ const result = {
254
+ directory,
255
+ script_types: script_types || Object.keys(SCRIPT_TYPES),
256
+ total_files: 0,
257
+ synced: 0,
258
+ failed: 0,
259
+ scripts: [],
260
+ timestamp: new Date().toISOString()
261
+ };
262
+
263
+ try {
264
+ // Ensure directory exists
265
+ await fs.mkdir(directory, { recursive: true });
266
+
267
+ // Read all files in directory
268
+ const files = await fs.readdir(directory);
269
+
270
+ // Filter for script files matching our naming convention
271
+ const scriptFiles = files.filter(file => {
272
+ const parsed = parseFileName(file);
273
+ if (!parsed.isValid) return false;
274
+
275
+ // Filter by script types if specified
276
+ if (script_types && !script_types.includes(parsed.scriptType)) {
277
+ return false;
278
+ }
279
+
280
+ return true;
281
+ });
282
+
283
+ result.total_files = scriptFiles.length;
284
+
285
+ // Sync each file
286
+ for (const file of scriptFiles) {
287
+ const parsed = parseFileName(file);
288
+ const filePath = path.join(directory, file);
289
+
290
+ try {
291
+ const syncResult = await syncScript(serviceNowClient, {
292
+ script_name: parsed.scriptName,
293
+ script_type: parsed.scriptType,
294
+ file_path: filePath,
295
+ direction: 'push', // Default to push for bulk sync
296
+ instance
297
+ });
298
+
299
+ result.scripts.push(syncResult);
300
+
301
+ if (syncResult.success) {
302
+ result.synced++;
303
+ } else {
304
+ result.failed++;
305
+ }
306
+ } catch (error) {
307
+ result.scripts.push({
308
+ script_name: parsed.scriptName,
309
+ script_type: parsed.scriptType,
310
+ file_path: filePath,
311
+ success: false,
312
+ error: error.message
313
+ });
314
+ result.failed++;
315
+ }
316
+ }
317
+
318
+ } catch (error) {
319
+ result.error = error.message;
320
+ }
321
+
322
+ return result;
323
+ }
324
+
325
+ /**
326
+ * Watch a directory for changes and auto-sync scripts
327
+ *
328
+ * NOTE: This function returns a watcher instance that runs in the background.
329
+ * The caller is responsible for managing the watcher lifecycle.
330
+ *
331
+ * @param {object} serviceNowClient - ServiceNow client instance
332
+ * @param {object} options - Watch options
333
+ * @param {string} options.directory - Directory to watch
334
+ * @param {string} options.script_type - Script type to watch (optional, defaults to all)
335
+ * @param {boolean} options.auto_sync - Auto-sync on file changes (default: true)
336
+ * @param {function} options.onSync - Callback function called after each sync
337
+ * @param {string} options.instance - ServiceNow instance name (optional)
338
+ * @returns {object} - { watcher, stop() }
339
+ */
340
+ export function watchScripts(serviceNowClient, options) {
341
+ const { directory, script_type, auto_sync = true, onSync, instance } = options;
342
+
343
+ // Track files being synced to prevent duplicate syncs
344
+ const syncingFiles = new Set();
345
+
346
+ // Create watcher
347
+ const watcher = chokidar.watch(directory, {
348
+ ignored: /(^|[\/\\])\../, // Ignore dot files
349
+ persistent: true,
350
+ ignoreInitial: true,
351
+ awaitWriteFinish: {
352
+ stabilityThreshold: 500,
353
+ pollInterval: 100
354
+ }
355
+ });
356
+
357
+ // Handle file changes
358
+ const handleFileChange = async (filePath) => {
359
+ // Prevent duplicate syncs
360
+ if (syncingFiles.has(filePath)) {
361
+ return;
362
+ }
363
+
364
+ const fileName = path.basename(filePath);
365
+ const parsed = parseFileName(fileName);
366
+
367
+ // Validate file name
368
+ if (!parsed.isValid) {
369
+ return;
370
+ }
371
+
372
+ // Filter by script type if specified
373
+ if (script_type && parsed.scriptType !== script_type) {
374
+ return;
375
+ }
376
+
377
+ // Mark as syncing
378
+ syncingFiles.add(filePath);
379
+
380
+ try {
381
+ if (auto_sync) {
382
+ const result = await syncScript(serviceNowClient, {
383
+ script_name: parsed.scriptName,
384
+ script_type: parsed.scriptType,
385
+ file_path: filePath,
386
+ direction: 'push',
387
+ instance
388
+ });
389
+
390
+ if (onSync) {
391
+ onSync(result);
392
+ }
393
+ }
394
+ } catch (error) {
395
+ if (onSync) {
396
+ onSync({
397
+ script_name: parsed.scriptName,
398
+ script_type: parsed.scriptType,
399
+ file_path: filePath,
400
+ success: false,
401
+ error: error.message
402
+ });
403
+ }
404
+ } finally {
405
+ // Remove from syncing set after delay
406
+ setTimeout(() => {
407
+ syncingFiles.delete(filePath);
408
+ }, 1000);
409
+ }
410
+ };
411
+
412
+ // Watch for file changes
413
+ watcher.on('add', handleFileChange);
414
+ watcher.on('change', handleFileChange);
415
+
416
+ // Watch for errors
417
+ watcher.on('error', (error) => {
418
+ console.error('Watcher error:', error);
419
+ });
420
+
421
+ // Return watcher control object
422
+ return {
423
+ watcher,
424
+ stop: () => {
425
+ return watcher.close();
426
+ }
427
+ };
428
+ }
package/src/server.js ADDED
@@ -0,0 +1,125 @@
1
+ /**
2
+ * ServiceNow MCP Server - Express HTTP Server
3
+ *
4
+ * Copyright (c) 2025 Happy Technologies LLC
5
+ * Licensed under the MIT License - see LICENSE file for details
6
+ */
7
+
8
+ import express from 'express';
9
+ import dotenv from 'dotenv';
10
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
11
+ import { ServiceNowClient } from './servicenow-client.js';
12
+ import { createMcpServer } from './mcp-server-consolidated.js';
13
+ import { configManager } from './config-manager.js';
14
+
15
+ // Load environment variables
16
+ dotenv.config();
17
+
18
+ const app = express();
19
+ app.use(express.json());
20
+
21
+ // In-memory session store (sessionId -> {server, transport})
22
+ const sessions = {};
23
+
24
+ // Get default instance configuration
25
+ const defaultInstance = configManager.getDefaultInstance();
26
+ console.log(`๐Ÿ”— Default ServiceNow instance: ${defaultInstance.name} (${defaultInstance.url})`);
27
+ console.log(`๐Ÿ’ก Use SN-Set-Instance tool to switch instances during session`);
28
+
29
+ // Create ServiceNow client with default instance
30
+ const serviceNowClient = new ServiceNowClient(
31
+ defaultInstance.url,
32
+ defaultInstance.username,
33
+ defaultInstance.password
34
+ );
35
+ serviceNowClient.currentInstanceName = defaultInstance.name;
36
+
37
+ /**
38
+ * GET /mcp - Establish SSE connection
39
+ */
40
+ app.get('/mcp', async (req, res) => {
41
+ try {
42
+ // Create transport and start SSE connection
43
+ const transport = new SSEServerTransport('/mcp', res);
44
+
45
+ // Create and configure new MCP server instance
46
+ const server = await createMcpServer(serviceNowClient);
47
+
48
+ // Set up transport cleanup
49
+ transport.onclose = () => {
50
+ if (sessions[transport.sessionId]) {
51
+ delete sessions[transport.sessionId];
52
+ console.log(`๐Ÿงน Cleaned up session ${transport.sessionId}`);
53
+ }
54
+ };
55
+
56
+ // Store the session
57
+ sessions[transport.sessionId] = { server, transport };
58
+ console.log(`๐Ÿ”— New session established: ${transport.sessionId}`);
59
+
60
+ // Connect server to transport and start SSE
61
+ await server.connect(transport);
62
+ await transport.start();
63
+
64
+ } catch (error) {
65
+ console.error('โŒ Error establishing SSE connection:', error);
66
+ res.status(500).json({ error: 'Failed to establish SSE connection' });
67
+ }
68
+ });
69
+
70
+ /**
71
+ * POST /mcp - Handle JSON-RPC messages
72
+ */
73
+ app.post('/mcp', async (req, res) => {
74
+ try {
75
+ const sessionId = req.query.sessionId;
76
+
77
+ if (!sessionId || !sessions[sessionId]) {
78
+ return res.status(400).json({
79
+ error: 'Invalid or missing session ID'
80
+ });
81
+ }
82
+
83
+ const { transport } = sessions[sessionId];
84
+ await transport.handlePostMessage(req, res);
85
+
86
+ } catch (error) {
87
+ console.error('โŒ Error handling POST message:', error);
88
+ res.status(500).json({ error: 'Failed to process message' });
89
+ }
90
+ });
91
+
92
+ // Health check endpoint
93
+ app.get('/health', (req, res) => {
94
+ res.json({
95
+ status: 'healthy',
96
+ servicenow_instance: defaultInstance.url,
97
+ instance_name: defaultInstance.name,
98
+ timestamp: new Date().toISOString()
99
+ });
100
+ });
101
+
102
+ // List available instances endpoint
103
+ app.get('/instances', (req, res) => {
104
+ try {
105
+ const instances = configManager.listInstances();
106
+ res.json({ instances });
107
+ } catch (error) {
108
+ res.status(500).json({ error: error.message });
109
+ }
110
+ });
111
+
112
+ // Start the server
113
+ const PORT = process.env.PORT || 3000;
114
+ app.listen(PORT, () => {
115
+ console.log(`ServiceNow MCP Server listening on port ${PORT}`);
116
+ console.log(`Health check: http://localhost:${PORT}/health`);
117
+ console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
118
+
119
+ console.log(`Available instances: http://localhost:${PORT}/instances`);
120
+
121
+ if (process.env.DEBUG === 'true') {
122
+ console.log('Debug mode enabled');
123
+ console.log(`Active ServiceNow instance: ${defaultInstance.name} - ${defaultInstance.url}`);
124
+ }
125
+ });