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
533 lines โข 22.1 kB
JavaScript
"use strict";
/**
* Base MCP Server Implementation
*
* Solves DRY violations by providing common functionality for all MCP servers:
* - Unified authentication handling
* - Consistent error handling
* - Session management
* - Logging and monitoring
* - Retry logic with exponential backoff
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseMCPServer = void 0;
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");
/**
* Base class for all ServiceNow MCP servers
* Provides common functionality to eliminate code duplication
*/
class BaseMCPServer {
constructor(config) {
this.tools = new Map();
// Performance tracking
this.toolMetrics = new Map();
// ๐ด SNOW-003 FIX: Circuit breaker implementation
this.circuitBreakers = new Map();
/**
* Tool handlers map
*/
this.toolHandlers = new Map();
// Store the config
this.config = config;
// Initialize server with config
this.server = new index_js_1.Server({
name: config.name,
version: config.version,
}, {
capabilities: config.capabilities || { tools: {} },
});
// Initialize common dependencies
this.client = new servicenow_client_js_1.ServiceNowClient();
this.oauth = new snow_oauth_js_1.ServiceNowOAuth();
this.logger = new logger_js_1.Logger(`MCP:${config.name}`);
this.transport = new stdio_js_1.StdioServerTransport();
// Setup common functionality
this.setupCommonHandlers();
this.setupAuthentication();
this.setupErrorHandling();
this.setupMetrics();
// Let child classes define their specific tools
this.setupTools();
}
/**
* Setup common request handlers
*/
setupCommonHandlers() {
// Handle tool listing
this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
tools: Array.from(this.tools.values()),
}));
// Handle tool execution with common auth/error handling
this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request, extra) => {
const { name, arguments: args } = request.params;
// Track metrics
const startTime = Date.now();
const metrics = this.toolMetrics.get(name) || { calls: 0, totalTime: 0, errors: 0 };
metrics.calls++;
try {
// Validate authentication first (skip if not required)
if (this.config.requiresAuth !== false) {
const authResult = await this.validateAuth();
if (!authResult.success) {
throw new types_js_1.McpError(types_js_1.ErrorCode.InvalidRequest, `Authentication failed: ${authResult.error}`);
}
}
// Execute tool with retry logic
const result = await this.executeWithRetry(name, args);
// Update metrics
metrics.totalTime += Date.now() - startTime;
this.toolMetrics.set(name, metrics);
return result;
}
catch (error) {
// Update error metrics
metrics.errors++;
metrics.totalTime += Date.now() - startTime;
this.toolMetrics.set(name, metrics);
throw error;
}
});
}
/**
* Setup authentication with automatic token refresh
*/
setupAuthentication() {
// Skip authentication setup if not required
if (this.config.requiresAuth === false) {
return;
}
// Check auth every 5 minutes
this.authCheckInterval = setInterval(async () => {
try {
await this.validateAuth();
}
catch (error) {
this.logger.error('Background auth check failed:', error);
}
}, 5 * 60 * 1000);
}
/**
* Validate authentication with smart caching
*/
async validateAuth() {
try {
// Check if we have a valid session
if (this.sessionToken && this.sessionExpiry && this.sessionExpiry > new Date()) {
return { success: true, token: this.sessionToken };
}
// Validate connection
const connectionResult = await this.validateServiceNowConnection();
if (!connectionResult.success) {
return {
success: false,
error: connectionResult.error || 'Authentication validation failed'
};
}
// Get fresh token
const isAuthenticated = await this.oauth.isAuthenticated();
if (!isAuthenticated) {
// Try to refresh
try {
await this.oauth.refreshAccessToken();
}
catch (refreshError) {
return {
success: false,
error: 'OAuth authentication required. Run "snow-flow auth login" to authenticate.',
};
}
}
// Cache session info
const tokenInfo = await this.oauth.loadTokens();
this.sessionToken = tokenInfo?.access_token;
this.sessionExpiry = new Date(Date.now() + (tokenInfo?.expires_in || 3600) * 1000);
return {
success: true,
token: this.sessionToken,
expiresIn: tokenInfo.expires_in,
};
}
catch (error) {
this.logger.error('Authentication validation failed:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown authentication error',
};
}
}
/**
* ๐ด SNOW-003 FIX: Enhanced retry logic with intelligent backoff and circuit breaker
* Addresses the 19% failure rate with better retry strategies and failure prevention
*/
async executeWithRetry(toolName, args, attempt = 1) {
// ๐ด CRITICAL: Increased retries from 3 to 6 for better resilience
const maxRetries = 6;
// ๐ด CRITICAL: Intelligent backoff based on error type
const backoffMs = this.calculateBackoff(attempt, toolName);
// ๐ด CRITICAL: Dynamic timeout based on tool complexity
const timeout = this.calculateTimeout(toolName);
try {
// Get tool handler
const handler = this.getToolHandler(toolName);
if (!handler) {
throw new types_js_1.McpError(types_js_1.ErrorCode.MethodNotFound, `Unknown tool: ${toolName}`);
}
// ๐ด CRITICAL: Memory usage check before execution
if (attempt === 1) {
await this.checkMemoryUsage();
}
// Execute with dynamic timeout
const result = await Promise.race([
handler(args),
new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms`)), timeout)),
]);
// ๐ด SUCCESS: Reset circuit breaker on success
this.resetCircuitBreaker(toolName);
return result;
}
catch (error) {
this.logger.error(`๐ด Tool ${toolName} execution failed (attempt ${attempt}/${maxRetries}):`, error);
// ๐ด CRITICAL: Update circuit breaker
this.updateCircuitBreaker(toolName, error);
// Check if retryable and within limits
if (attempt < maxRetries && this.isRetryableError(error) && !this.isCircuitBreakerOpen(toolName)) {
this.logger.info(`๐ Retrying ${toolName} after ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
await new Promise(resolve => setTimeout(resolve, backoffMs));
return this.executeWithRetry(toolName, args, attempt + 1);
}
// ๐ด FINAL FAILURE: Enhanced error reporting
const errorMessage = this.createEnhancedErrorMessage(toolName, error, attempt, maxRetries);
throw new types_js_1.McpError(types_js_1.ErrorCode.InternalError, errorMessage);
}
}
/**
* ๐ด SNOW-003 FIX: Calculate intelligent backoff based on error type and attempt
*/
calculateBackoff(attempt, toolName) {
// Base exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s
const baseBackoff = 1000 * Math.pow(2, attempt - 1);
// Add jitter to prevent thundering herd (ยฑ25%)
const jitter = baseBackoff * 0.25 * (Math.random() - 0.5);
// Cap maximum backoff at 30 seconds
const maxBackoff = 30000;
return Math.min(baseBackoff + jitter, maxBackoff);
}
/**
* ๐ด SNOW-003 FIX: Calculate dynamic timeout based on tool complexity
*/
calculateTimeout(toolName) {
// Tool-specific timeouts based on complexity
const timeoutMap = {
'snow_create_flow': 90000, // Flow creation: 90s
'snow_deploy': 120000, // Deployment: 2 minutes
'snow_deploy_widget': 180000, // Widget deployment: 3 minutes (complex ML widgets)
'snow_create_widget': 120000, // Widget creation: 2 minutes
'ml_train_incident_classifier': 300000, // ML training: 5 minutes
'ml_train_change_risk': 300000, // ML training: 5 minutes
'snow_comprehensive_search': 45000, // Search: 45s
'snow_find_artifact': 30000, // Find: 30s
'snow_validate_live_connection': 15000, // Validation: 15s
};
// Default timeout for unknown tools
return timeoutMap[toolName] || 60000; // 60s default (increased from 30s)
}
updateCircuitBreaker(toolName, error) {
const breaker = this.circuitBreakers.get(toolName) || { failures: 0, lastFailure: 0, isOpen: false };
breaker.failures++;
breaker.lastFailure = Date.now();
// Open circuit breaker after 5 failures within 5 minutes
if (breaker.failures >= 5 && (Date.now() - breaker.lastFailure) < 300000) {
breaker.isOpen = true;
this.logger.warn(`๐จ Circuit breaker opened for ${toolName} due to repeated failures`);
}
this.circuitBreakers.set(toolName, breaker);
}
isCircuitBreakerOpen(toolName) {
const breaker = this.circuitBreakers.get(toolName);
if (!breaker || !breaker.isOpen)
return false;
// Auto-reset circuit breaker after 10 minutes
if (Date.now() - breaker.lastFailure > 600000) {
breaker.isOpen = false;
breaker.failures = 0;
this.circuitBreakers.set(toolName, breaker);
this.logger.info(`โ
Circuit breaker reset for ${toolName}`);
return false;
}
return true;
}
resetCircuitBreaker(toolName) {
const breaker = this.circuitBreakers.get(toolName);
if (breaker) {
breaker.failures = 0;
breaker.isOpen = false;
this.circuitBreakers.set(toolName, breaker);
}
}
/**
* ๐ด SNOW-003 FIX: Memory usage monitoring to prevent memory exhaustion failures
*/
async checkMemoryUsage() {
try {
const memUsage = process.memoryUsage();
const heapUsedMB = Math.round(memUsage.heapUsed / 1024 / 1024);
const heapTotalMB = Math.round(memUsage.heapTotal / 1024 / 1024);
// Log memory usage if high (>200MB)
if (heapUsedMB > 200) {
this.logger.warn(`โ ๏ธ High memory usage: ${heapUsedMB}MB heap used, ${heapTotalMB}MB total`);
}
// Trigger garbage collection if memory usage is very high (>500MB)
if (heapUsedMB > 500 && global.gc) {
this.logger.info('๐งน Triggering garbage collection due to high memory usage');
global.gc();
}
// Fail fast if memory usage is critical (>800MB)
if (heapUsedMB > 800) {
throw new Error(`Critical memory usage: ${heapUsedMB}MB. Operation aborted to prevent system instability.`);
}
}
catch (error) {
this.logger.warn('Could not check memory usage:', error);
}
}
/**
* ๐ด SNOW-003 FIX: Enhanced error message with troubleshooting guidance
*/
createEnhancedErrorMessage(toolName, error, attempts, maxRetries) {
const baseMessage = `Tool '${toolName}' failed after ${attempts}/${maxRetries} attempts`;
const errorDetail = error instanceof Error ? error.message : String(error);
let troubleshooting = '';
// Add specific troubleshooting based on error type
if (error.response?.status === 401) {
troubleshooting = '\n๐ก Authentication issue: Run "snow-flow auth login" to re-authenticate';
}
else if (error.response?.status === 403) {
troubleshooting = '\n๐ก Permission issue: Check ServiceNow user permissions and OAuth scopes';
}
else if (error.response?.status >= 500) {
troubleshooting = '\n๐ก ServiceNow server issue: Try again later or contact ServiceNow administrator';
}
else if (errorDetail.includes('timeout')) {
troubleshooting = '\n๐ก Timeout issue: ServiceNow instance may be slow - try again later';
}
else if (errorDetail.includes('network') || errorDetail.includes('connection')) {
troubleshooting = '\n๐ก Network issue: Check internet connection and ServiceNow instance availability';
}
return `${baseMessage}: ${errorDetail}${troubleshooting}`;
}
/**
* ๐ด SNOW-003 FIX: Enhanced error classification for ServiceNow specific errors
* Addresses the 19% failure rate by properly categorizing retryable errors
*/
isRetryableError(error) {
if (error instanceof Error) {
const message = error.message.toLowerCase();
// ๐ด CRITICAL: ServiceNow specific retryable errors
const serviceNowRetryable = (message.includes('timeout') ||
message.includes('econnreset') ||
message.includes('socket hang up') ||
message.includes('enotfound') ||
message.includes('rate limit') ||
message.includes('service unavailable') ||
message.includes('bad gateway') ||
message.includes('gateway timeout') ||
message.includes('connection refused') ||
message.includes('network error') ||
message.includes('dns lookup failed') ||
message.includes('connect etimedout') ||
message.includes('index not available') ||
message.includes('search index updating') ||
message.includes('temporary failure') ||
message.includes('server is busy') ||
message.includes('database lock') ||
message.includes('deadlock detected'));
// ๐ด CRITICAL: HTTP status code based retry logic
if (error.response?.status) {
const status = error.response.status;
const httpRetryable = (status === 429 || // Rate limit
status === 502 || // Bad Gateway
status === 503 || // Service Unavailable
status === 504 || // Gateway Timeout
status === 507 || // Insufficient Storage
status === 520 || // CloudFlare unknown error
status === 521 || // Web server is down
status === 522 || // Connection timed out
status === 523 || // Origin is unreachable
status === 524 // A timeout occurred
);
// 401 is retryable only once (for token refresh)
const authRetryable = status === 401 && !error.config?._retry;
return httpRetryable || authRetryable;
}
return serviceNowRetryable;
}
// Handle specific error types
if (error.code) {
const retryableCodes = [
'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
'ESOCKETTIMEDOUT', 'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN'
];
return retryableCodes.includes(error.code);
}
return false;
}
/**
* Setup global error handling
*/
setupErrorHandling() {
process.on('uncaughtException', (error) => {
this.logger.error('Uncaught exception:', error);
this.gracefulShutdown();
});
process.on('unhandledRejection', (reason, promise) => {
this.logger.error('Unhandled rejection:', { promise, reason });
});
process.on('SIGINT', () => {
this.logger.info('Received SIGINT, shutting down gracefully...');
this.gracefulShutdown();
});
}
/**
* Setup metrics collection
*/
setupMetrics() {
// Log metrics every minute
setInterval(() => {
const metrics = Array.from(this.toolMetrics.entries()).map(([tool, data]) => ({
tool,
calls: data.calls,
avgTime: data.calls > 0 ? Math.round(data.totalTime / data.calls) : 0,
errorRate: data.calls > 0 ? (data.errors / data.calls * 100).toFixed(2) : '0',
}));
if (metrics.length > 0) {
this.logger.info('Tool metrics:', metrics);
}
}, 60000);
}
/**
* Execute tool with common error handling
*/
async executeTool(toolName, handler) {
const startTime = Date.now();
try {
// Validate auth before execution
const authResult = await this.validateAuth();
if (!authResult.success) {
return {
success: false,
error: authResult.error,
retryable: true,
};
}
// Execute the tool logic
const result = await handler();
// Log success
this.logger.debug(`Tool ${toolName} executed successfully in ${Date.now() - startTime}ms`);
return {
success: true,
result,
executionTime: Date.now() - startTime,
};
}
catch (error) {
this.logger.error(`Tool ${toolName} failed:`, error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
retryable: this.isRetryableError(error),
executionTime: Date.now() - startTime,
};
}
}
/**
* Register a tool
*/
registerTool(tool, handler) {
this.tools.set(tool.name, tool);
this.toolHandlers.set(tool.name, handler);
}
/**
* Get tool handler
*/
getToolHandler(name) {
return this.toolHandlers.get(name);
}
/**
* Graceful shutdown
*/
async gracefulShutdown() {
this.logger.info('Starting graceful shutdown...');
// Clear intervals
if (this.authCheckInterval) {
clearInterval(this.authCheckInterval);
}
// Log final metrics
const metrics = Array.from(this.toolMetrics.entries());
if (metrics.length > 0) {
this.logger.info('Final metrics:', metrics);
}
// Close connections
try {
await this.oauth.logout();
}
catch (error) {
this.logger.error('Error during logout:', error);
}
process.exit(0);
}
/**
* Start the server
*/
async start() {
this.logger.info(`Starting ${this.config.name} v${this.config.version}`);
// Validate initial connection (skip if not required)
if (this.config.requiresAuth !== false) {
const authResult = await this.validateAuth();
if (!authResult.success) {
this.logger.warn('Starting without authentication - some features may be limited');
}
}
await this.server.connect(this.transport);
this.logger.info('MCP server started successfully');
}
/**
* Abstract method for child classes to implement their specific tools
*/
/**
* Validate ServiceNow connection
*/
async validateServiceNowConnection() {
try {
const isAuthenticated = await this.oauth.isAuthenticated();
if (!isAuthenticated) {
return {
success: false,
error: 'Not authenticated with ServiceNow'
};
}
// Test connection with a simple request
const response = await this.client.makeRequest({
method: 'GET',
url: '/api/now/table/sys_properties',
params: { sysparm_limit: 1 }
});
return {
success: response.success,
token: 'valid'
};
}
catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Connection validation failed'
};
}
}
}
exports.BaseMCPServer = BaseMCPServer;
//# sourceMappingURL=base-mcp-server.js.map