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.
- package/.claude/settings.local.json +70 -0
- package/CLAUDE.md +777 -0
- package/LICENSE +21 -0
- package/README.md +562 -0
- package/assets/logo.svg +385 -0
- package/config/servicenow-instances.json.example +28 -0
- package/docs/403_TROUBLESHOOTING.md +329 -0
- package/docs/API_REFERENCE.md +1142 -0
- package/docs/APPLICATION_SCOPE_VALIDATION.md +681 -0
- package/docs/CLAUDE_DESKTOP_SETUP.md +373 -0
- package/docs/CONVENIENCE_TOOLS.md +601 -0
- package/docs/CONVENIENCE_TOOLS_SUMMARY.md +371 -0
- package/docs/FLOW_DESIGNER_GUIDE.md +1021 -0
- package/docs/IMPLEMENTATION_COMPLETE.md +165 -0
- package/docs/INSTANCE_SWITCHING_GUIDE.md +219 -0
- package/docs/MULTI_INSTANCE_CONFIGURATION.md +185 -0
- package/docs/NATURAL_LANGUAGE_SEARCH_IMPLEMENTATION.md +221 -0
- package/docs/PUPPETEER_INTEGRATION_PROPOSAL.md +1322 -0
- package/docs/QUICK_REFERENCE.md +395 -0
- package/docs/README.md +75 -0
- package/docs/RESOURCES_ARCHITECTURE.md +392 -0
- package/docs/RESOURCES_IMPLEMENTATION.md +276 -0
- package/docs/RESOURCES_SUMMARY.md +104 -0
- package/docs/SETUP_GUIDE.md +104 -0
- package/docs/UI_OPERATIONS_ARCHITECTURE.md +1219 -0
- package/docs/UI_OPERATIONS_DECISION_MATRIX.md +542 -0
- package/docs/UI_OPERATIONS_SUMMARY.md +507 -0
- package/docs/UPDATE_SET_VALIDATION.md +598 -0
- package/docs/UPDATE_SET_VALIDATION_SUMMARY.md +209 -0
- package/docs/VALIDATION_SUMMARY.md +479 -0
- package/jest.config.js +24 -0
- package/package.json +61 -0
- package/scripts/background_script_2025-09-29T20-19-35-101Z.js +23 -0
- package/scripts/link_ui_policy_actions_2025-09-29T20-17-15-218Z.js +90 -0
- package/scripts/set_update_set_Integration_Governance_Framework_2025-09-29T19-47-06-790Z.js +30 -0
- package/scripts/set_update_set_Integration_Governance_Framework_2025-09-29T19-59-33-152Z.js +30 -0
- package/scripts/set_update_set_current_2025-09-29T20-16-59-675Z.js +24 -0
- package/scripts/test_sys_dictionary_403.js +85 -0
- package/setup/setup-report.json +5313 -0
- package/src/config/comprehensive-table-definitions.json +2575 -0
- package/src/config/instance-config.json +4693 -0
- package/src/config/prompts.md +59 -0
- package/src/config/table-definitions.json +4681 -0
- package/src/config-manager.js +146 -0
- package/src/mcp-server-consolidated.js +2894 -0
- package/src/natural-language.js +472 -0
- package/src/resources.js +326 -0
- package/src/script-sync.js +428 -0
- package/src/server.js +125 -0
- package/src/servicenow-client.js +1625 -0
- package/src/stdio-server.js +52 -0
- 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
|
+
});
|