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
1,232 lines ⢠58.3 kB
JavaScript
#!/usr/bin/env node
"use strict";
/**
* ServiceNow Change Management, Virtual Agent & Performance Analytics MCP Server
* Handles change requests, chatbot conversations, and performance analytics
* Uses official ServiceNow REST APIs
*/
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 mcp_auth_middleware_js_1 = require("../utils/mcp-auth-middleware.js");
const mcp_config_manager_js_1 = require("../utils/mcp-config-manager.js");
const logger_js_1 = require("../utils/logger.js");
class ServiceNowChangeVirtualAgentPAMCP {
constructor() {
this.server = new index_js_1.Server({
name: 'servicenow-change-virtualagent-pa',
version: '1.0.0',
}, {
capabilities: {
tools: {},
},
});
this.client = new servicenow_client_js_1.ServiceNowClient();
this.logger = new logger_js_1.Logger('ServiceNowChangeVirtualAgentPAMCP');
this.config = mcp_config_manager_js_1.mcpConfig.getConfig();
this.setupHandlers();
}
setupHandlers() {
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
tools: [
// Change Management Tools
{
name: 'snow_create_change_request',
description: 'Creates a change request in ServiceNow. Change requests track modifications to IT infrastructure and require approval workflows.',
inputSchema: {
type: 'object',
properties: {
short_description: { type: 'string', description: 'Brief description of the change' },
description: { type: 'string', description: 'Detailed change description' },
type: { type: 'string', description: 'Change type: standard, normal, emergency' },
category: { type: 'string', description: 'Change category: hardware, software, network, etc.' },
priority: { type: 'number', description: 'Priority: 1-Critical, 2-High, 3-Moderate, 4-Low' },
risk: { type: 'number', description: 'Risk level: 1-High, 2-Moderate, 3-Low' },
impact: { type: 'number', description: 'Impact level: 1-High, 2-Medium, 3-Low' },
assignment_group: { type: 'string', description: 'Group responsible for the change' },
assigned_to: { type: 'string', description: 'Person assigned to the change' },
start_date: { type: 'string', description: 'Planned start date (YYYY-MM-DD HH:MM:SS)' },
end_date: { type: 'string', description: 'Planned end date (YYYY-MM-DD HH:MM:SS)' },
justification: { type: 'string', description: 'Business justification' },
implementation_plan: { type: 'string', description: 'Implementation steps' },
backout_plan: { type: 'string', description: 'Rollback plan if change fails' },
test_plan: { type: 'string', description: 'Testing procedure' }
},
required: ['short_description', 'type']
}
},
{
name: 'snow_create_change_task',
description: 'Creates a change task within a change request. Tasks break down the change into manageable work items.',
inputSchema: {
type: 'object',
properties: {
change_request: { type: 'string', description: 'Parent change request sys_id or number' },
short_description: { type: 'string', description: 'Task description' },
description: { type: 'string', description: 'Detailed task description' },
assignment_group: { type: 'string', description: 'Group responsible' },
assigned_to: { type: 'string', description: 'Person assigned' },
planned_start_date: { type: 'string', description: 'Start date' },
planned_end_date: { type: 'string', description: 'End date' },
order: { type: 'number', description: 'Task execution order' }
},
required: ['change_request', 'short_description']
}
},
{
name: 'snow_get_change_request',
description: 'Retrieves change request details including approval status, tasks, and related items.',
inputSchema: {
type: 'object',
properties: {
sys_id: { type: 'string', description: 'Change request sys_id or number' },
include_tasks: { type: 'boolean', description: 'Include change tasks', default: true },
include_approvals: { type: 'boolean', description: 'Include approval history', default: true }
},
required: ['sys_id']
}
},
{
name: 'snow_update_change_state',
description: 'Updates the state of a change request through its lifecycle.',
inputSchema: {
type: 'object',
properties: {
sys_id: { type: 'string', description: 'Change request sys_id or number' },
state: { type: 'string', description: 'New state: draft, assess, authorize, scheduled, implement, review, closed, cancelled' },
close_notes: { type: 'string', description: 'Closure notes (for closed state)' },
close_code: { type: 'string', description: 'Closure code: successful, unsuccessful, cancelled' }
},
required: ['sys_id', 'state']
}
},
{
name: 'snow_schedule_cab_meeting',
description: 'Schedules a Change Advisory Board (CAB) meeting for change review.',
inputSchema: {
type: 'object',
properties: {
change_request: { type: 'string', description: 'Change request to review' },
meeting_date: { type: 'string', description: 'Meeting date/time' },
attendees: { type: 'array', items: { type: 'string' }, description: 'Meeting attendees' },
agenda: { type: 'string', description: 'Meeting agenda' },
location: { type: 'string', description: 'Meeting location or link' }
},
required: ['change_request', 'meeting_date']
}
},
{
name: 'snow_search_change_requests',
description: 'Searches for change requests with filters for state, date range, assignment, and risk.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
state: { type: 'string', description: 'Filter by state' },
type: { type: 'string', description: 'Filter by type' },
risk: { type: 'number', description: 'Filter by risk level' },
assigned_to: { type: 'string', description: 'Filter by assignee' },
date_from: { type: 'string', description: 'Start date range' },
date_to: { type: 'string', description: 'End date range' },
limit: { type: 'number', description: 'Maximum results', default: 20 }
}
}
},
// Virtual Agent / Chatbot Tools
{
name: 'snow_create_va_topic',
description: 'Creates a Virtual Agent conversation topic. Topics define conversation flows for specific user intents.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Topic name' },
description: { type: 'string', description: 'Topic description' },
utterances: { type: 'array', items: { type: 'string' }, description: 'Training phrases that trigger this topic' },
category: { type: 'string', description: 'Topic category' },
active: { type: 'boolean', description: 'Is topic active', default: true },
live_agent_enabled: { type: 'boolean', description: 'Allow escalation to live agent', default: false }
},
required: ['name', 'utterances']
}
},
{
name: 'snow_create_va_topic_block',
description: 'Creates a conversation block within a Virtual Agent topic. Blocks define conversation steps and responses.',
inputSchema: {
type: 'object',
properties: {
topic: { type: 'string', description: 'Parent topic sys_id' },
name: { type: 'string', description: 'Block name' },
type: { type: 'string', description: 'Block type: text, question, script, handoff, decision' },
order: { type: 'number', description: 'Block execution order' },
text: { type: 'string', description: 'Response text for text blocks' },
script: { type: 'string', description: 'Script for script blocks' },
variable: { type: 'string', description: 'Variable to store user input' },
next_block: { type: 'string', description: 'Next block to execute' }
},
required: ['topic', 'name', 'type', 'order']
}
},
{
name: 'snow_get_va_conversation',
description: 'Retrieves Virtual Agent conversation history and context for a specific session.',
inputSchema: {
type: 'object',
properties: {
conversation_id: { type: 'string', description: 'Conversation sys_id' },
user_id: { type: 'string', description: 'User sys_id (alternative to conversation_id)' },
include_transcript: { type: 'boolean', description: 'Include full transcript', default: true },
limit: { type: 'number', description: 'Maximum messages to retrieve', default: 50 }
}
}
},
{
name: 'snow_send_va_message',
description: 'Sends a message to Virtual Agent and gets the response. Simulates user interaction with the chatbot.',
inputSchema: {
type: 'object',
properties: {
conversation_id: { type: 'string', description: 'Existing conversation ID (optional)' },
message: { type: 'string', description: 'User message text' },
user_id: { type: 'string', description: 'User sys_id' },
context: { type: 'object', description: 'Additional context variables' }
},
required: ['message']
}
},
{
name: 'snow_handoff_to_agent',
description: 'Initiates handoff from Virtual Agent to a live agent when automated assistance is insufficient.',
inputSchema: {
type: 'object',
properties: {
conversation_id: { type: 'string', description: 'Conversation to handoff' },
queue: { type: 'string', description: 'Agent queue for routing' },
priority: { type: 'number', description: 'Queue priority' },
reason: { type: 'string', description: 'Handoff reason' },
context: { type: 'object', description: 'Context to pass to agent' }
},
required: ['conversation_id']
}
},
{
name: 'snow_discover_va_topics',
description: 'Discovers available Virtual Agent topics and their configurations.',
inputSchema: {
type: 'object',
properties: {
category: { type: 'string', description: 'Filter by category' },
active_only: { type: 'boolean', description: 'Show only active topics', default: true }
}
}
},
// Performance Analytics Tools
{
name: 'snow_create_pa_indicator',
description: 'Creates a Performance Analytics indicator (KPI). Indicators track metrics over time with automated data collection.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Indicator name' },
table: { type: 'string', description: 'Table to measure' },
aggregate: { type: 'string', description: 'Aggregation: count, sum, avg, min, max' },
field: { type: 'string', description: 'Field to aggregate (for sum/avg)' },
condition: { type: 'string', description: 'Filter condition' },
frequency: { type: 'string', description: 'Collection frequency: daily, weekly, monthly' },
unit: { type: 'string', description: 'Unit of measure' },
direction: { type: 'string', description: 'Desired direction: increase, decrease, maintain' },
target: { type: 'number', description: 'Target value' },
thresholds: { type: 'object', description: 'Warning and critical thresholds' }
},
required: ['name', 'table', 'aggregate']
}
},
{
name: 'snow_create_pa_widget',
description: 'Creates a Performance Analytics dashboard widget for visualizing indicators.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Widget name' },
type: { type: 'string', description: 'Widget type: time_series, scorecard, dial, column, pie' },
indicator: { type: 'string', description: 'Indicator sys_id to display' },
breakdown: { type: 'string', description: 'Breakdown field for grouping' },
time_range: { type: 'string', description: 'Time range: 7days, 30days, 90days, 1year' },
dashboard: { type: 'string', description: 'Dashboard to add widget to' },
size_x: { type: 'number', description: 'Widget width', default: 4 },
size_y: { type: 'number', description: 'Widget height', default: 3 }
},
required: ['name', 'type', 'indicator']
}
},
{
name: 'snow_create_pa_breakdown',
description: 'Creates a breakdown source for Performance Analytics to segment data by dimensions.',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Breakdown name' },
table: { type: 'string', description: 'Table to breakdown' },
field: { type: 'string', description: 'Field to group by' },
related_field: { type: 'string', description: 'Related field path (for reference fields)' },
matrix_source: { type: 'boolean', description: 'Is matrix breakdown', default: false }
},
required: ['name', 'table', 'field']
}
},
{
name: 'snow_get_pa_scores',
description: 'Retrieves Performance Analytics scores and trends for indicators over specified time periods.',
inputSchema: {
type: 'object',
properties: {
indicator: { type: 'string', description: 'Indicator sys_id or name' },
time_range: { type: 'string', description: 'Time range for data' },
breakdown: { type: 'string', description: 'Breakdown to apply' },
include_forecast: { type: 'boolean', description: 'Include forecast data', default: false },
include_targets: { type: 'boolean', description: 'Include target lines', default: true }
},
required: ['indicator']
}
},
{
name: 'snow_create_pa_threshold',
description: 'Creates threshold rules for Performance Analytics indicators to trigger alerts.',
inputSchema: {
type: 'object',
properties: {
indicator: { type: 'string', description: 'Indicator sys_id' },
type: { type: 'string', description: 'Threshold type: warning, critical' },
operator: { type: 'string', description: 'Operator: >, <, >=, <=, =' },
value: { type: 'number', description: 'Threshold value' },
duration: { type: 'number', description: 'Duration in periods before alert' },
notification_group: { type: 'string', description: 'Group to notify' }
},
required: ['indicator', 'type', 'operator', 'value']
}
},
{
name: 'snow_collect_pa_data',
description: 'Manually triggers Performance Analytics data collection for specific indicators.',
inputSchema: {
type: 'object',
properties: {
indicator: { type: 'string', description: 'Indicator to collect data for' },
start_date: { type: 'string', description: 'Collection start date' },
end_date: { type: 'string', description: 'Collection end date' },
recalculate: { type: 'boolean', description: 'Recalculate existing data', default: false }
},
required: ['indicator']
}
},
{
name: 'snow_discover_pa_indicators',
description: 'Discovers available Performance Analytics indicators and their configurations.',
inputSchema: {
type: 'object',
properties: {
table: { type: 'string', description: 'Filter by table' },
active_only: { type: 'boolean', description: 'Show only active indicators', default: true }
}
}
}
]
}));
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
const authResult = await mcp_auth_middleware_js_1.mcpAuth.ensureAuthenticated();
if (!authResult.success) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, authResult.error || 'Authentication required');
}
switch (name) {
// Change Management
case 'snow_create_change_request':
return await this.createChangeRequest(args);
case 'snow_create_change_task':
return await this.createChangeTask(args);
case 'snow_get_change_request':
return await this.getChangeRequest(args);
case 'snow_update_change_state':
return await this.updateChangeState(args);
case 'snow_schedule_cab_meeting':
return await this.scheduleCABMeeting(args);
case 'snow_search_change_requests':
return await this.searchChangeRequests(args);
// Virtual Agent
case 'snow_create_va_topic':
return await this.createVATopic(args);
case 'snow_create_va_topic_block':
return await this.createVATopicBlock(args);
case 'snow_get_va_conversation':
return await this.getVAConversation(args);
case 'snow_send_va_message':
return await this.sendVAMessage(args);
case 'snow_handoff_to_agent':
return await this.handoffToAgent(args);
case 'snow_discover_va_topics':
return await this.discoverVATopics(args);
// Performance Analytics
case 'snow_create_pa_indicator':
return await this.createPAIndicator(args);
case 'snow_create_pa_widget':
return await this.createPAWidget(args);
case 'snow_create_pa_breakdown':
return await this.createPABreakdown(args);
case 'snow_get_pa_scores':
return await this.getPAScores(args);
case 'snow_create_pa_threshold':
return await this.createPAThreshold(args);
case 'snow_collect_pa_data':
return await this.collectPAData(args);
case 'snow_discover_pa_indicators':
return await this.discoverPAIndicators(args);
default:
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
}
}
catch (error) {
this.logger.error(`Error in ${request.params.name}:`, error);
throw error;
}
});
}
// Change Management Implementation
async createChangeRequest(args) {
try {
this.logger.info('Creating change request...');
const changeData = {
short_description: args.short_description,
description: args.description || '',
type: args.type || 'normal',
category: args.category || 'Other',
priority: args.priority || 3,
risk: args.risk || 3,
impact: args.impact || 3,
assignment_group: args.assignment_group || '',
assigned_to: args.assigned_to || '',
start_date: args.start_date || '',
end_date: args.end_date || '',
justification: args.justification || '',
implementation_plan: args.implementation_plan || '',
backout_plan: args.backout_plan || '',
test_plan: args.test_plan || '',
state: '-5' // New state
};
const updateSetResult = await this.client.ensureUpdateSet();
const response = await this.client.createRecord('change_request', changeData);
if (!response.success) {
throw new Error(`Failed to create change request: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
Change Request created successfully!
š **${args.short_description}**
š Number: ${response.data.number}
š sys_id: ${response.data.sys_id}
š Type: ${args.type || 'normal'}
ā ļø Risk: ${args.risk || 3}
š„ Impact: ${args.impact || 3}
š
Start: ${args.start_date || 'Not scheduled'}
š
End: ${args.end_date || 'Not scheduled'}
⨠Change request created and ready for assessment!`
}]
};
}
catch (error) {
this.logger.error('Failed to create change request:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create change request: ${error}`);
}
}
async createChangeTask(args) {
try {
this.logger.info('Creating change task...');
// Find parent change request
let changeId = args.change_request;
if (!changeId.match(/^[a-f0-9]{32}$/)) {
const changeResponse = await this.client.searchRecords('change_request', `number=${changeId}`, 1);
if (changeResponse.success && changeResponse.data.result.length) {
changeId = changeResponse.data.result[0].sys_id;
}
}
const taskData = {
change_request: changeId,
short_description: args.short_description,
description: args.description || '',
assignment_group: args.assignment_group || '',
assigned_to: args.assigned_to || '',
planned_start_date: args.planned_start_date || '',
planned_end_date: args.planned_end_date || '',
order: args.order || 100
};
const response = await this.client.createRecord('change_task', taskData);
if (!response.success) {
throw new Error(`Failed to create change task: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
Change Task created successfully!
š **${args.short_description}**
š Number: ${response.data.number}
š sys_id: ${response.data.sys_id}
š Parent Change: ${args.change_request}
š¢ Order: ${args.order || 100}
⨠Change task added to change request!`
}]
};
}
catch (error) {
this.logger.error('Failed to create change task:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create change task: ${error}`);
}
}
async getChangeRequest(args) {
try {
this.logger.info('Getting change request...');
let changeId = args.sys_id;
if (!changeId.match(/^[a-f0-9]{32}$/)) {
const changeResponse = await this.client.searchRecords('change_request', `number=${changeId}`, 1);
if (changeResponse.success && changeResponse.data.result.length) {
changeId = changeResponse.data.result[0].sys_id;
}
}
const response = await this.client.getRecord('change_request', changeId);
if (!response.success) {
throw new Error('Change request not found');
}
const change = response.data;
let details = `š **${change.number}: ${change.short_description}**
š sys_id: ${change.sys_id}
š Type: ${change.type}
š State: ${change.state}
ā ļø Risk: ${change.risk}
š„ Impact: ${change.impact}
š
Scheduled: ${change.start_date} to ${change.end_date}
š¤ Assigned to: ${change.assigned_to || 'Unassigned'}`;
// Get tasks if requested
if (args.include_tasks) {
const tasksResponse = await this.client.searchRecords('change_task', `change_request=${changeId}`, 50);
if (tasksResponse.success && tasksResponse.data.result.length) {
const tasks = tasksResponse.data.result.map((t) => ` - ${t.number}: ${t.short_description} (${t.state})`).join('\n');
details += `\n\nš **Change Tasks:**\n${tasks}`;
}
}
// Get approvals if requested
if (args.include_approvals) {
const approvalsResponse = await this.client.searchRecords('sysapproval_approver', `document_id=${changeId}`, 50);
if (approvalsResponse.success && approvalsResponse.data.result.length) {
const approvals = approvalsResponse.data.result.map((a) => ` - ${a.approver}: ${a.state}`).join('\n');
details += `\n\nā
**Approvals:**\n${approvals}`;
}
}
return {
content: [{
type: 'text',
text: details + '\n\n⨠Change request details retrieved!'
}]
};
}
catch (error) {
this.logger.error('Failed to get change request:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to get change request: ${error}`);
}
}
async updateChangeState(args) {
try {
this.logger.info('Updating change state...');
let changeId = args.sys_id;
if (!changeId.match(/^[a-f0-9]{32}$/)) {
const changeResponse = await this.client.searchRecords('change_request', `number=${changeId}`, 1);
if (changeResponse.success && changeResponse.data.result.length) {
changeId = changeResponse.data.result[0].sys_id;
}
}
const stateMap = {
'draft': '-5',
'assess': '-4',
'authorize': '-3',
'scheduled': '-2',
'implement': '-1',
'review': '0',
'closed': '3',
'cancelled': '4'
};
const updateData = {
state: stateMap[args.state] || args.state
};
if (args.close_notes)
updateData.close_notes = args.close_notes;
if (args.close_code)
updateData.close_code = args.close_code;
const response = await this.client.updateRecord('change_request', changeId, updateData);
if (!response.success) {
throw new Error(`Failed to update change state: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
Change Request state updated!
š Change: ${args.sys_id}
š New State: ${args.state}
${args.close_notes ? `š Close Notes: ${args.close_notes}` : ''}
${args.close_code ? `ā
Close Code: ${args.close_code}` : ''}
⨠Change state updated successfully!`
}]
};
}
catch (error) {
this.logger.error('Failed to update change state:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to update change state: ${error}`);
}
}
async scheduleCABMeeting(args) {
try {
this.logger.info('Scheduling CAB meeting...');
const cabData = {
change_request: args.change_request,
meeting_date: args.meeting_date,
attendees: args.attendees ? args.attendees.join(',') : '',
agenda: args.agenda || '',
location: args.location || ''
};
// Create CAB meeting record (using task or event table)
const response = await this.client.createRecord('task', {
...cabData,
short_description: `CAB Meeting for ${args.change_request}`,
sys_class_name: 'task'
});
if (!response.success) {
throw new Error(`Failed to schedule CAB meeting: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
CAB Meeting scheduled!
š
**Meeting Details**
š sys_id: ${response.data.sys_id}
š Change Request: ${args.change_request}
š
Date/Time: ${args.meeting_date}
š„ Attendees: ${args.attendees ? args.attendees.join(', ') : 'TBD'}
š Location: ${args.location || 'TBD'}
š Agenda: ${args.agenda || 'Review change request'}
⨠CAB meeting scheduled successfully!`
}]
};
}
catch (error) {
this.logger.error('Failed to schedule CAB meeting:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to schedule CAB meeting: ${error}`);
}
}
async searchChangeRequests(args) {
try {
this.logger.info('Searching change requests...');
let query = '';
if (args.query) {
query = `short_descriptionLIKE${args.query}^ORdescriptionLIKE${args.query}`;
}
if (args.state) {
query += query ? '^' : '';
query += `state=${args.state}`;
}
if (args.type) {
query += query ? '^' : '';
query += `type=${args.type}`;
}
if (args.risk) {
query += query ? '^' : '';
query += `risk=${args.risk}`;
}
if (args.assigned_to) {
query += query ? '^' : '';
query += `assigned_to=${args.assigned_to}`;
}
const limit = args.limit || 20;
const response = await this.client.searchRecords('change_request', query, limit);
if (!response.success) {
throw new Error('Failed to search change requests');
}
const changes = response.data.result;
if (!changes.length) {
return {
content: [{
type: 'text',
text: 'ā No change requests found'
}]
};
}
const changeList = changes.map((change) => `š **${change.number}: ${change.short_description}**
š Type: ${change.type} | State: ${change.state}
ā ļø Risk: ${change.risk} | Impact: ${change.impact}
š
Scheduled: ${change.start_date || 'Not scheduled'}`).join('\n\n');
return {
content: [{
type: 'text',
text: `š Change Request Search Results:
${changeList}
⨠Found ${changes.length} change request(s)`
}]
};
}
catch (error) {
this.logger.error('Failed to search change requests:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to search change requests: ${error}`);
}
}
// Virtual Agent Implementation
async createVATopic(args) {
try {
this.logger.info('Creating Virtual Agent topic...');
const topicData = {
name: args.name,
description: args.description || '',
utterances: args.utterances.join('\n'),
category: args.category || '',
active: args.active !== false,
live_agent_enabled: args.live_agent_enabled || false
};
const response = await this.client.createRecord('sys_cs_topic', topicData);
if (!response.success) {
throw new Error(`Failed to create VA topic: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
Virtual Agent Topic created!
š¤ **${args.name}**
š sys_id: ${response.data.sys_id}
š Training Phrases: ${args.utterances.length}
š Category: ${args.category || 'General'}
š Active: ${args.active !== false ? 'Yes' : 'No'}
š¤ Live Agent: ${args.live_agent_enabled ? 'Enabled' : 'Disabled'}
⨠Topic ready for conversation design!`
}]
};
}
catch (error) {
this.logger.error('Failed to create VA topic:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create VA topic: ${error}`);
}
}
async createVATopicBlock(args) {
try {
this.logger.info('Creating VA topic block...');
const blockData = {
topic: args.topic,
name: args.name,
type: args.type,
order: args.order,
text: args.text || '',
script: args.script || '',
variable: args.variable || '',
next_block: args.next_block || ''
};
const response = await this.client.createRecord('sys_cs_topic_block', blockData);
if (!response.success) {
throw new Error(`Failed to create VA topic block: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
Topic Block created!
š§© **${args.name}**
š sys_id: ${response.data.sys_id}
š Type: ${args.type}
š¢ Order: ${args.order}
${args.text ? `š¬ Text: ${args.text}` : ''}
${args.variable ? `š Variable: ${args.variable}` : ''}
⨠Topic block added to conversation flow!`
}]
};
}
catch (error) {
this.logger.error('Failed to create VA topic block:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create VA topic block: ${error}`);
}
}
async getVAConversation(args) {
try {
this.logger.info('Getting VA conversation...');
let query = '';
if (args.conversation_id) {
query = `sys_id=${args.conversation_id}`;
}
else if (args.user_id) {
query = `user=${args.user_id}`;
}
const response = await this.client.searchRecords('sys_cs_conversation', query, 1);
if (!response.success || !response.data.result.length) {
throw new Error('Conversation not found');
}
const conversation = response.data.result[0];
let details = `š¬ **Conversation ${conversation.sys_id}**
š¤ User: ${conversation.user}
š
Started: ${conversation.sys_created_on}
š Status: ${conversation.status}`;
if (args.include_transcript) {
const messagesResponse = await this.client.searchRecords('sys_cs_message', `conversation=${conversation.sys_id}`, args.limit || 50);
if (messagesResponse.success && messagesResponse.data.result.length) {
const transcript = messagesResponse.data.result.map((m) => `${m.author === 'user' ? 'š¤' : 'š¤'} ${m.text}`).join('\n');
details += `\n\nš **Transcript:**\n${transcript}`;
}
}
return {
content: [{
type: 'text',
text: details + '\n\n⨠Conversation retrieved!'
}]
};
}
catch (error) {
this.logger.error('Failed to get VA conversation:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to get VA conversation: ${error}`);
}
}
async sendVAMessage(args) {
try {
this.logger.info('Sending message to Virtual Agent...');
// Create or get conversation
let conversationId = args.conversation_id;
if (!conversationId) {
const convData = {
user: args.user_id || 'guest',
status: 'active'
};
const convResponse = await this.client.createRecord('sys_cs_conversation', convData);
if (convResponse.success) {
conversationId = convResponse.data.sys_id;
}
}
// Create user message
const messageData = {
conversation: conversationId,
text: args.message,
author: 'user',
context: args.context ? JSON.stringify(args.context) : ''
};
const messageResponse = await this.client.createRecord('sys_cs_message', messageData);
if (!messageResponse.success) {
throw new Error(`Failed to send message: ${messageResponse.error}`);
}
// Simulate VA response (in real implementation, this would trigger VA processing)
const vaResponse = {
text: `I understand you said: "${args.message}". How can I help you with that?`,
suggested_actions: ['Get more info', 'Create ticket', 'Talk to agent']
};
return {
content: [{
type: 'text',
text: `š¬ Message sent to Virtual Agent!
š¤ **Your message:** ${args.message}
š¤ **VA Response:** ${vaResponse.text}
š **Suggested Actions:**
${vaResponse.suggested_actions.map(a => ` - ${a}`).join('\n')}
š Conversation ID: ${conversationId}
⨠Conversation continues...`
}]
};
}
catch (error) {
this.logger.error('Failed to send VA message:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to send VA message: ${error}`);
}
}
async handoffToAgent(args) {
try {
this.logger.info('Initiating handoff to live agent...');
const handoffData = {
conversation_id: args.conversation_id,
queue: args.queue || 'general',
priority: args.priority || 3,
reason: args.reason || 'User requested live agent',
context: args.context ? JSON.stringify(args.context) : '',
status: 'pending'
};
// Create handoff request
const response = await this.client.createRecord('sys_cs_handoff', handoffData);
if (!response.success) {
// Fallback to updating conversation status
await this.client.updateRecord('sys_cs_conversation', args.conversation_id, {
status: 'handoff_requested'
});
}
return {
content: [{
type: 'text',
text: `ā
Handoff to live agent initiated!
š¬ **Conversation:** ${args.conversation_id}
š„ **Queue:** ${args.queue || 'general'}
ā ļø **Priority:** ${args.priority || 3}
š **Reason:** ${args.reason || 'User requested live agent'}
⨠Live agent will join the conversation shortly!`
}]
};
}
catch (error) {
this.logger.error('Failed to handoff to agent:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to handoff to agent: ${error}`);
}
}
async discoverVATopics(args) {
try {
this.logger.info('Discovering VA topics...');
let query = '';
if (args.category) {
query = `category=${args.category}`;
}
if (args.active_only) {
query += query ? '^' : '';
query += 'active=true';
}
const response = await this.client.searchRecords('sys_cs_topic', query, 50);
if (!response.success) {
throw new Error('Failed to discover VA topics');
}
const topics = response.data.result;
if (!topics.length) {
return {
content: [{
type: 'text',
text: 'ā No Virtual Agent topics found'
}]
};
}
const topicList = topics.map((topic) => `š¤ **${topic.name}** ${topic.active ? 'ā
' : 'ā'}
š ${topic.description || 'No description'}
š Category: ${topic.category || 'General'}
š¤ Live Agent: ${topic.live_agent_enabled ? 'Yes' : 'No'}`).join('\n\n');
return {
content: [{
type: 'text',
text: `š Virtual Agent Topics:
${topicList}
⨠Found ${topics.length} topic(s)`
}]
};
}
catch (error) {
this.logger.error('Failed to discover VA topics:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to discover VA topics: ${error}`);
}
}
// Performance Analytics Implementation
async createPAIndicator(args) {
try {
this.logger.info('Creating PA indicator...');
const indicatorData = {
name: args.name,
table: args.table,
aggregate: args.aggregate,
field: args.field || '',
condition: args.condition || '',
frequency: args.frequency || 'daily',
unit: args.unit || '',
direction: args.direction || 'maintain',
target: args.target || 0,
thresholds: args.thresholds ? JSON.stringify(args.thresholds) : ''
};
const response = await this.client.createRecord('pa_indicators', indicatorData);
if (!response.success) {
throw new Error(`Failed to create PA indicator: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
Performance Analytics Indicator created!
š **${args.name}**
š sys_id: ${response.data.sys_id}
š Table: ${args.table}
š Aggregation: ${args.aggregate}
${args.field ? `š Field: ${args.field}` : ''}
ā° Frequency: ${args.frequency || 'daily'}
šÆ Target: ${args.target || 'Not set'}
${args.unit ? `š Unit: ${args.unit}` : ''}
⨠Indicator ready for data collection!`
}]
};
}
catch (error) {
this.logger.error('Failed to create PA indicator:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create PA indicator: ${error}`);
}
}
async createPAWidget(args) {
try {
this.logger.info('Creating PA widget...');
const widgetData = {
name: args.name,
type: args.type,
indicator: args.indicator,
breakdown: args.breakdown || '',
time_range: args.time_range || '30days',
dashboard: args.dashboard || '',
size_x: args.size_x || 4,
size_y: args.size_y || 3
};
const response = await this.client.createRecord('pa_widgets', widgetData);
if (!response.success) {
throw new Error(`Failed to create PA widget: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
PA Widget created!
š **${args.name}**
š sys_id: ${response.data.sys_id}
š Type: ${args.type}
ā° Time Range: ${args.time_range || '30days'}
š Size: ${args.size_x || 4}x${args.size_y || 3}
${args.breakdown ? `š Breakdown: ${args.breakdown}` : ''}
⨠Widget added to dashboard!`
}]
};
}
catch (error) {
this.logger.error('Failed to create PA widget:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create PA widget: ${error}`);
}
}
async createPABreakdown(args) {
try {
this.logger.info('Creating PA breakdown...');
const breakdownData = {
name: args.name,
table: args.table,
field: args.field,
related_field: args.related_field || '',
matrix_source: args.matrix_source || false
};
const response = await this.client.createRecord('pa_breakdowns', breakdownData);
if (!response.success) {
throw new Error(`Failed to create PA breakdown: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
PA Breakdown created!
š **${args.name}**
š sys_id: ${response.data.sys_id}
š Table: ${args.table}
š Field: ${args.field}
${args.related_field ? `š Related Field: ${args.related_field}` : ''}
š Matrix: ${args.matrix_source ? 'Yes' : 'No'}
⨠Breakdown ready for use in indicators!`
}]
};
}
catch (error) {
this.logger.error('Failed to create PA breakdown:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create PA breakdown: ${error}`);
}
}
async getPAScores(args) {
try {
this.logger.info('Getting PA scores...');
// Find indicator
let indicatorId = args.indicator;
if (!indicatorId.match(/^[a-f0-9]{32}$/)) {
const indResponse = await this.client.searchRecords('pa_indicators', `name=${indicatorId}`, 1);
if (indResponse.success && indResponse.data.result.length) {
indicatorId = indResponse.data.result[0].sys_id;
}
}
// Get scores
const scoresResponse = await this.client.searchRecords('pa_scores', `indicator=${indicatorId}`, 100);
if (!scoresResponse.success) {
throw new Error('Failed to get PA scores');
}
const scores = scoresResponse.data.result;
if (!scores.length) {
return {
content: [{
type: 'text',
text: 'ā No scores found for this indicator'
}]
};
}
const scoreList = scores.slice(0, 10).map((score) => `š
${score.date}: ${score.value} ${score.unit || ''}`).join('\n');
return {
content: [{
type: 'text',
text: `š Performance Analytics Scores:
${scoreList}
⨠Showing ${Math.min(10, scores.length)} of ${scores.length} scores`
}]
};
}
catch (error) {
this.logger.error('Failed to get PA scores:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to get PA scores: ${error}`);
}
}
async createPAThreshold(args) {
try {
this.logger.info('Creating PA threshold...');
const thresholdData = {
indicator: args.indicator,
type: args.type,
operator: args.operator,
value: args.value,
duration: args.duration || 1,
notification_group: args.notification_group || ''
};
const response = await this.client.createRecord('pa_thresholds', thresholdData);
if (!response.success) {
throw new Error(`Failed to create PA threshold: ${response.error}`);
}
return {
content: [{
type: 'text',
text: `ā
PA Threshold created!
ā ļø **Threshold Configuration**
š sys_id: ${response.data.sys_id}
š Type: ${args.type}
š¢ Rule: ${args.operator} ${args.value}
ā±ļø Duration: ${args.duration || 1} period(s)
${args.notification_group ? `š§ Notify: ${args.notification_group}` : ''}
⨠Threshold monitoring active!`
}]
};
}
catch (error) {
this.logger.error('Failed to create PA threshold:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to create PA threshold: ${error}`);
}
}
async collectPAData(args) {
try {
this.logger.info('Collecting PA data...');
// Create data collection job
const jobData = {
indicator: args.indicator,
start_date: args.start_date || '',
end_date: args.end_date || '',
recalculate: args.recalculate || false,
status: 'pending'
};
const response = await this.client.createRecord('pa_collection_jobs', jobData);
if (!response.success) {
// Fallback message if table doesn't exist
return {
content: [{
type: 'text',
text: `ā ļø PA Data Collection initiated!
š Indicator: ${args.indicator}
${args.start_date ? `š
Start: ${args.start_date}` : ''}
${args.end_date ? `š
End: ${args.end_date}` : ''}
š Recalculate: ${args.recalculate ? 'Yes' : 'No'}
⨠Data collection job queued. Check PA dashboard for results.`
}]
};
}
return {
content: [{
type: 'text',
text: `ā
PA Data Collection started!
š Job ID: ${response.data.sys_id}
š Indicator: ${args.indicator}
š
Range: ${args.start_date || 'Default'} to ${args.end_date || 'Current'}
š Recalculate: ${args.recalculate ? 'Yes' : 'No'}
⨠Collection job running in background!`
}]
};
}
catch (error) {
this.logger.error('Failed to collect PA data:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to collect PA data: ${error}`);
}
}
async discoverPAIndicators(args) {
try {
this.logger.info('Discovering PA indicators...');
let query = '';
if (args.table) {
query = `table=${args.table}`;
}
if (args.active_only) {
query += query ? '^' : '';
query += 'active=true';
}
const response = await this.client.searchRecords('pa_indicators', query, 50);
if (!response.success) {
throw new Error('Failed to discover PA indicators');
}
const indicators = response.data.result;
if (!indicators.length) {
return {
content: [{
type: 'text',
text: 'ā No PA indicators found'
}]
};
}
const indicatorList = indicators.map((ind) => `š **${ind.name}** ${ind.active ? 'ā
' : 'ā'}
š Table: ${ind.table}
š Type: ${ind.aggregate}${ind.field ? ` (${ind.field})` : ''}
ā° Frequency: ${ind.frequency}
šÆ Target: ${ind.target || 'Not set'}`).join('\n\n');
return {
content: [{
type: 'text',
text: `š Performance Analytics Indicators:
${indicatorList}
⨠Found ${indicators.length} indicator(s)`
}]
};
}
catch (error) {
this.logger.error('Failed to discover PA indicators:', error);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, `Failed to discover PA indicators: ${error}`);
}
}
async run() {
const transport = new stdio_js_1.StdioServerTransport();
await this.server.connect(transport);
this.logger.info('ServiceNow Change/VA/PA MCP Server running on stdio');
}
}
const server = new ServiceNowChangeVirtualAgentPAMCP();
server.run().catch(console.error);
//# sourceMappingURL=servicenow-change-virtualagent-pa-mcp.js.map