snow-flow
Version:
Snow-Flow v3.2.0: Complete ServiceNow Enterprise Suite with 180+ MCP Tools. ATF Testing, Knowledge Management, Service Catalog, Change Management with CAB scheduling, Virtual Agent chatbots with NLU, Performance Analytics KPIs, Flow Designer automation, A
722 lines (696 loc) ⢠31 kB
JavaScript
"use strict";
/**
* ServiceNow Update Set Management MCP Server
* Ensures all changes are tracked in Update Sets for safe deployment
*/
Object.defineProperty(exports, "__esModule", { value: true });
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
const servicenow_client_js_1 = require("../utils/servicenow-client.js");
const snow_oauth_js_1 = require("../utils/snow-oauth.js");
const logger_js_1 = require("../utils/logger.js");
const fs_1 = require("fs");
const path_1 = require("path");
class ServiceNowUpdateSetMCP {
constructor() {
this.currentSession = null;
this.server = new index_js_1.Server({
name: 'servicenow-update-set',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.client = new servicenow_client_js_1.ServiceNowClient();
this.oauth = new snow_oauth_js_1.ServiceNowOAuth();
this.logger = new logger_js_1.Logger('ServiceNowUpdateSetMCP');
this.sessionsPath = (0, path_1.join)(process.cwd(), 'memory', 'update-set-sessions');
// Debug: Test credentials on startup
this.testCredentials();
this.setupHandlers();
this.ensureSessionsDirectory();
}
/**
* Test credentials on startup
*/
async testCredentials() {
console.log('š [UPDATE-SET MCP] Testing credentials...');
try {
const credentials = await this.oauth.loadCredentials();
if (credentials) {
console.log('ā
[UPDATE-SET MCP] Credentials loaded successfully');
const isAuth = await this.oauth.isAuthenticated();
console.log(`š [UPDATE-SET MCP] Authentication status: ${isAuth ? 'ā
Valid' : 'ā Expired'}`);
}
else {
console.log('ā [UPDATE-SET MCP] No credentials found');
}
}
catch (error) {
console.error('ā [UPDATE-SET MCP] Credential test failed:', error);
}
}
async ensureSessionsDirectory() {
try {
await fs_1.promises.mkdir(this.sessionsPath, { recursive: true });
}
catch (error) {
this.logger.error('Failed to create sessions directory', error);
}
}
setupHandlers() {
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
tools: [
{
name: 'snow_update_set_create',
description: 'Creates a new Update Set for tracking changes related to a user story or feature. Essential for ServiceNow change management and deployment tracking.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Update Set name (e.g., "STORY-123: Add incident widget")'
},
description: {
type: 'string',
description: 'Detailed description of changes'
},
user_story: {
type: 'string',
description: 'User story or ticket number'
},
release_date: {
type: 'string',
description: 'Target release date (optional)'
},
auto_switch: {
type: 'boolean',
description: 'Automatically switch to the created Update Set (default: true)',
default: true
}
},
required: ['name', 'description']
}
},
{
name: 'snow_update_set_switch',
description: 'Switches the active Update Set context to an existing set. Ensures all subsequent changes are tracked in the specified Update Set.',
inputSchema: {
type: 'object',
properties: {
update_set_id: {
type: 'string',
description: 'Update Set sys_id to switch to'
}
},
required: ['update_set_id']
}
},
{
name: 'snow_update_set_current',
description: 'Retrieves information about the currently active Update Set including ID, name, state, and tracked artifacts.',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'snow_update_set_list',
description: 'Lists Update Sets filtered by state (in_progress, complete, released). Provides overview of recent changes and deployment readiness.',
inputSchema: {
type: 'object',
properties: {
state: {
type: 'string',
description: 'Filter by state: in_progress, complete, released',
enum: ['in_progress', 'complete', 'released']
},
limit: {
type: 'number',
description: 'Maximum number of results (default: 10)'
}
}
}
},
{
name: 'snow_update_set_complete',
description: 'Marks an Update Set as complete, preventing further changes. Prepares the set for testing, review, and migration to other instances.',
inputSchema: {
type: 'object',
properties: {
update_set_id: {
type: 'string',
description: 'Update Set sys_id to complete (uses current if not specified)'
},
notes: {
type: 'string',
description: 'Completion notes or testing instructions'
}
}
}
},
{
name: 'snow_update_set_add_artifact',
description: 'Registers an artifact (widget, flow, script) in the active Update Set for tracking. Maintains comprehensive change history for deployments.',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
description: 'Artifact type (widget, flow, script, etc.)'
},
sys_id: {
type: 'string',
description: 'ServiceNow sys_id of the artifact'
},
name: {
type: 'string',
description: 'Artifact name for tracking'
}
},
required: ['type', 'sys_id', 'name']
}
},
{
name: 'snow_update_set_preview',
description: 'Generates a detailed preview of all changes contained in an Update Set. Shows modified tables, fields, and potential deployment impacts.',
inputSchema: {
type: 'object',
properties: {
update_set_id: {
type: 'string',
description: 'Update Set sys_id (uses current if not specified)'
}
}
}
},
{
name: 'snow_update_set_export',
description: 'Exports Update Set to XML format for backup, version control, or manual migration between instances. Preserves all change records and metadata.',
inputSchema: {
type: 'object',
properties: {
update_set_id: {
type: 'string',
description: 'Update Set sys_id to export'
},
output_path: {
type: 'string',
description: 'Path to save the XML file'
}
}
}
},
{
name: 'snow_ensure_active_update_set',
description: 'Ensures an active Update Set is available for tracking changes. Automatically creates a contextual Update Set if none exists, preventing untracked modifications.',
inputSchema: {
type: 'object',
properties: {
context: {
type: 'string',
description: 'Context for auto-created Update Set (e.g., "widget development", "flow creation")'
},
auto_create: {
type: 'boolean',
description: 'Automatically create Update Set if none exists (default: true)',
default: true
}
}
}
}
]
}));
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
// Check authentication for all operations
const isAuthenticated = await this.oauth.isAuthenticated();
if (!isAuthenticated) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, 'Not authenticated. Run "snow-flow auth login" first.');
}
switch (name) {
case 'snow_update_set_create':
return await this.createUpdateSet(args);
case 'snow_update_set_switch':
return await this.switchUpdateSet(args);
case 'snow_update_set_current':
return await this.getCurrentUpdateSet();
case 'snow_update_set_list':
return await this.listUpdateSets(args);
case 'snow_update_set_complete':
return await this.completeUpdateSet(args);
case 'snow_update_set_add_artifact':
return await this.addArtifactToSession(args);
case 'snow_update_set_preview':
return await this.previewUpdateSet(args);
case 'snow_update_set_export':
return await this.exportUpdateSet(args);
case 'snow_ensure_active_update_set':
return await this.ensureActiveUpdateSet(args);
default:
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
if (error instanceof types_js_1.McpError)
throw error;
this.logger.error('Tool execution failed', { tool: name, error });
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`);
}
});
}
async createUpdateSet(args) {
try {
this.logger.info('Creating new Update Set', args);
// Create Update Set in ServiceNow
const response = await this.client.createUpdateSet({
name: args.name,
description: args.description,
release_date: args.release_date,
state: 'in_progress'
});
if (!response.success) {
throw new Error(response.error || 'Failed to create Update Set');
}
// Validate response structure
if (!response.data || !response.data.sys_id) {
throw new Error(`Invalid Update Set response: missing data or sys_id. Response: ${JSON.stringify(response)}`);
}
// Auto-switch to Update Set if requested (default: true)
const autoSwitch = args.auto_switch !== false;
let switchedToUpdateSet = false;
if (autoSwitch) {
await this.client.setCurrentUpdateSet(response.data.sys_id);
switchedToUpdateSet = true;
// Create local session
this.currentSession = {
update_set_id: response.data.sys_id,
name: args.name,
description: args.description,
user_story: args.user_story,
created_at: new Date().toISOString(),
state: 'in_progress',
artifacts: [],
auto_switched: true,
active_session: true
};
// Save session
await this.saveSession();
}
const credentials = await this.oauth.loadCredentials();
const updateSetUrl = `https://${credentials?.instance}/sys_update_set.do?sys_id=${response.data.sys_id}`;
return {
content: [{
type: 'text',
text: `ā
**Update Set Created Successfully!**
š **Details:**
- **Name**: ${args.name}
- **ID**: ${response.data.sys_id}
- **Description**: ${args.description}
${args.user_story ? `- **User Story**: ${args.user_story}` : ''}
- **State**: In Progress
${switchedToUpdateSet ? '- **Auto-Switched**: ā
Active session ready' : '- **Auto-Switch**: ā Manual switch required'}
š **View in ServiceNow**: ${updateSetUrl}
${switchedToUpdateSet ? `ā” **Current Session Active**
All subsequent changes will be automatically tracked in this Update Set.` : `ā ļø **Manual Switch Required**
Use \`snow_update_set_switch\` to activate this Update Set before making changes.`}
š” **Best Practices:**
1. Keep Update Sets focused on a single story/feature
2. Test thoroughly before marking complete
3. Document all changes in the description
4. Use meaningful names that include story numbers`
}]
};
}
catch (error) {
this.logger.error('Failed to create Update Set', error);
throw error;
}
}
async switchUpdateSet(args) {
try {
this.logger.info('Switching to Update Set', { update_set_id: args.update_set_id });
// Set as current in ServiceNow
await this.client.setCurrentUpdateSet(args.update_set_id);
// Load or create session
const sessionFile = (0, path_1.join)(this.sessionsPath, `${args.update_set_id}.json`);
try {
const sessionData = await fs_1.promises.readFile(sessionFile, 'utf-8');
this.currentSession = JSON.parse(sessionData);
}
catch {
// Create new session for existing Update Set
const updateSet = await this.client.getUpdateSet(args.update_set_id);
this.currentSession = {
update_set_id: args.update_set_id,
name: updateSet.data.name,
description: updateSet.data.description,
created_at: updateSet.data.sys_created_on,
state: updateSet.data.state,
artifacts: []
};
await this.saveSession();
}
return {
content: [{
type: 'text',
text: `ā
**Switched to Update Set**
š **Current Update Set:**
- **Name**: ${this.currentSession?.name || 'Unknown'}
- **ID**: ${this.currentSession?.update_set_id || 'Unknown'}
- **State**: ${this.currentSession?.state || 'Unknown'}
- **Artifacts Tracked**: ${this.currentSession?.artifacts.length || 0}
All subsequent changes will be tracked in this Update Set.`
}]
};
}
catch (error) {
this.logger.error('Failed to switch Update Set', error);
throw error;
}
}
async getCurrentUpdateSet() {
if (!this.currentSession) {
// Try to get from ServiceNow
const current = await this.client.getCurrentUpdateSet();
if (current.success && current.data) {
return {
content: [{
type: 'text',
text: `š **Current Update Set (from ServiceNow):**
- **Name**: ${current.data.name}
- **ID**: ${current.data.sys_id}
- **State**: ${current.data.state}
ā ļø **Note**: No local session active. Use \`snow_update_set_switch\` to activate session tracking.`
}]
};
}
return {
content: [{
type: 'text',
text: 'ā **No Update Set Active**\n\nUse `snow_update_set_create` to create a new Update Set for your changes.'
}]
};
}
return {
content: [{
type: 'text',
text: `š **Current Update Set Session:**
- **Name**: ${this.currentSession.name}
- **ID**: ${this.currentSession.update_set_id}
- **User Story**: ${this.currentSession.user_story || 'Not specified'}
- **State**: ${this.currentSession.state}
- **Created**: ${new Date(this.currentSession.created_at).toLocaleString()}
- **Artifacts**: ${this.currentSession.artifacts.length}
š¦ **Tracked Artifacts:**
${this.currentSession.artifacts.length > 0
? this.currentSession.artifacts.map(a => `- ${a.type}: ${a.name}`).join('\n')
: '- No artifacts tracked yet'}`
}]
};
}
async listUpdateSets(args) {
try {
const response = await this.client.listUpdateSets({
state: args.state,
limit: args.limit || 10
});
if (!response.success) {
throw new Error(response.error || 'Failed to list Update Sets');
}
const updateSets = response.data || [];
const credentials = await this.oauth.loadCredentials();
return {
content: [{
type: 'text',
text: `š **Update Sets** (${updateSets.length} found)
${updateSets.map((us) => `
**${us.name}**
- ID: ${us.sys_id}
- State: ${us.state}
- Created: ${new Date(us.sys_created_on).toLocaleDateString()}
- Created By: ${us.sys_created_by}
- š [View](https://${credentials?.instance}/sys_update_set.do?sys_id=${us.sys_id})
`).join('\n---\n')}
š” Use \`snow_update_set_switch\` to activate any Update Set.`
}]
};
}
catch (error) {
this.logger.error('Failed to list Update Sets', error);
throw error;
}
}
async completeUpdateSet(args) {
try {
const updateSetId = args.update_set_id || this.currentSession?.update_set_id;
if (!updateSetId) {
throw new Error('No Update Set specified and no active session');
}
// Mark as complete in ServiceNow
const response = await this.client.completeUpdateSet(updateSetId, args.notes);
if (!response.success) {
throw new Error(response.error || 'Failed to complete Update Set');
}
// Update session
if (this.currentSession && this.currentSession.update_set_id === updateSetId) {
this.currentSession.state = 'complete';
await this.saveSession();
}
const credentials = await this.oauth.loadCredentials();
const updateSetUrl = `https://${credentials?.instance}/sys_update_set.do?sys_id=${updateSetId}`;
return {
content: [{
type: 'text',
text: `ā
**Update Set Completed!**
š **Summary:**
- **Name**: ${response.data.name}
- **ID**: ${updateSetId}
- **State**: Complete
${args.notes ? `- **Notes**: ${args.notes}` : ''}
š **View in ServiceNow**: ${updateSetUrl}
š **Next Steps:**
1. Test all changes thoroughly
2. Get peer review if required
3. Move to target instance when ready
4. Create new Update Set for next feature
ā ļø **Important**: This Update Set is now locked. Create a new one for additional changes.`
}]
};
}
catch (error) {
this.logger.error('Failed to complete Update Set', error);
throw error;
}
}
async addArtifactToSession(args) {
// Intelligent session management - auto-create session if none exists
if (!this.currentSession) {
this.logger.info('No active session found, auto-creating Update Set session');
try {
// Create a default update set with smart naming
const defaultName = `AUTO-${new Date().toISOString().split('T')[0]}-${Date.now().toString().slice(-6)}`;
const defaultDescription = `Auto-created Update Set for ${args.type} deployment: ${args.name}`;
await this.createUpdateSet({
name: defaultName,
description: defaultDescription,
user_story: 'Automated artifact deployment'
});
this.logger.info('Auto-created Update Set session', {
name: defaultName,
updateSetId: this.currentSession?.update_set_id
});
}
catch (error) {
this.logger.error('Failed to auto-create Update Set session', { error });
throw new Error(`No active Update Set session and auto-creation failed: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Add artifact to session
this.currentSession.artifacts.push({
type: args.type,
sys_id: args.sys_id,
name: args.name,
created_at: new Date().toISOString()
});
await this.saveSession();
const autoCreatedNotice = this.currentSession.name.startsWith('AUTO-')
? `\nš **Smart Session Management:**\n- ā
Update Set session auto-created (no manual setup required)\n- š Naming: ${this.currentSession.name}\n- šÆ Intelligent deployment tracking enabled\n`
: '';
return {
content: [{
type: 'text',
text: `ā
**Artifact Added to Update Set Session**
š¦ **Artifact Details:**
- **Type**: ${args.type}
- **Name**: ${args.name}
- **Sys ID**: ${args.sys_id}
${autoCreatedNotice}
š **Current Session:**
- **Update Set**: ${this.currentSession.name}
- **Total Artifacts**: ${this.currentSession.artifacts.length}
- **Session ID**: ${this.currentSession.update_set_id}`
}]
};
}
async previewUpdateSet(args) {
try {
const updateSetId = args.update_set_id || this.currentSession?.update_set_id;
if (!updateSetId) {
throw new Error('No Update Set specified and no active session');
}
// Get Update Set details and changes
const response = await this.client.previewUpdateSet(updateSetId);
if (!response.success) {
throw new Error(response.error || 'Failed to preview Update Set');
}
const changes = response.data.changes || [];
const credentials = await this.oauth.loadCredentials();
return {
content: [{
type: 'text',
text: `š **Update Set Preview**
**Update Set**: ${response.data.name}
**Total Changes**: ${changes.length}
š¦ **Changes by Type:**
${this.groupChangesByType(changes)}
š **Change Details:**
${changes.slice(0, 20).map((change) => `
- **${change.type}**: ${change.target_name}
- Action: ${change.action}
- Table: ${change.target_table}
- Updated: ${new Date(change.sys_updated_on).toLocaleString()}
`).join('\n')}
${changes.length > 20 ? `\n... and ${changes.length - 20} more changes` : ''}
š **Full Preview**: https://${credentials?.instance}/sys_update_set_preview.do?sysparm_set=${updateSetId}`
}]
};
}
catch (error) {
this.logger.error('Failed to preview Update Set', error);
throw error;
}
}
async exportUpdateSet(args) {
try {
const updateSetId = args.update_set_id;
if (!updateSetId) {
throw new Error('Update Set ID is required for export');
}
// Export Update Set as XML
const response = await this.client.exportUpdateSet(updateSetId);
if (!response.success) {
throw new Error(response.error || 'Failed to export Update Set');
}
// Save to file
const outputPath = args.output_path || (0, path_1.join)(process.cwd(), 'exports', `update_set_${updateSetId}_${Date.now()}.xml`);
await fs_1.promises.mkdir((0, path_1.join)(process.cwd(), 'exports'), { recursive: true });
await fs_1.promises.writeFile(outputPath, response.data.xml);
return {
content: [{
type: 'text',
text: `ā
**Update Set Exported Successfully!**
š¦ **Export Details:**
- **Update Set**: ${response.data.name}
- **File Size**: ${(response.data.xml.length / 1024).toFixed(2)} KB
- **Changes**: ${response.data.change_count}
- **Saved to**: ${outputPath}
š” **Usage:**
- Import this XML file to another ServiceNow instance
- Keep as backup before major changes
- Share with team members for review`
}]
};
}
catch (error) {
this.logger.error('Failed to export Update Set', error);
throw error;
}
}
async ensureActiveUpdateSet(args) {
try {
this.logger.info('Ensuring active Update Set session', args);
// Check if we already have an active session
if (this.currentSession?.state === 'in_progress') {
return {
content: [{
type: 'text',
text: `ā
**Active Update Set Session Found**
š **Current Session:**
- **Name**: ${this.currentSession.name}
- **ID**: ${this.currentSession.update_set_id}
- **Created**: ${new Date(this.currentSession.created_at).toLocaleString()}
- **Artifacts**: ${this.currentSession.artifacts?.length || 0} tracked
ā” **Ready for Deployment**
All subsequent changes will be tracked in this Update Set.`
}]
};
}
// Auto-create if requested (default: true)
const autoCreate = args.auto_create !== false;
if (autoCreate) {
const context = args.context || 'automated deployment';
const timestamp = new Date().toISOString().slice(0, 16).replace('T', ' ');
return await this.createUpdateSet({
name: `Auto-${context} (${timestamp})`,
description: `Automatically created Update Set for ${context}`,
user_story: 'Automated deployment workflow',
auto_switch: true
});
}
else {
return {
content: [{
type: 'text',
text: `ā **No Active Update Set Session**
ā ļø **Manual Creation Required**
Create an Update Set before making changes:
\`\`\`
snow_update_set_create({
name: "Your feature name",
description: "Description of changes"
})
\`\`\`
š” **Why Update Sets Matter:**
- Track all changes for rollback capability
- Organize related changes together
- Required for deployment to other environments`
}]
};
}
}
catch (error) {
this.logger.error('Failed to ensure active Update Set', error);
throw error;
}
}
async saveSession() {
if (!this.currentSession)
return;
const sessionFile = (0, path_1.join)(this.sessionsPath, `${this.currentSession.update_set_id}.json`);
await fs_1.promises.writeFile(sessionFile, JSON.stringify(this.currentSession, null, 2));
}
groupChangesByType(changes) {
const grouped = changes.reduce((acc, change) => {
const type = change.type || 'Other';
acc[type] = (acc[type] || 0) + 1;
return acc;
}, {});
return Object.entries(grouped)
.sort((a, b) => b[1] - a[1])
.map(([type, count]) => `- ${type}: ${count}`)
.join('\n');
}
async run() {
const transport = new stdio_js_1.StdioServerTransport();
await this.server.connect(transport);
this.logger.info('ServiceNow Update Set MCP Server running on stdio');
}
}
const server = new ServiceNowUpdateSetMCP();
server.run().catch(console.error);
//# sourceMappingURL=servicenow-update-set-mcp.js.map