mcp-servicenow
Version:
ServiceNow MCP server for Claude AI integration
262 lines (225 loc) • 9.04 kB
text/typescript
import * as dotenv from 'dotenv';
dotenv.config();
import { log } from './utils/logger';
import { JsonRpcMessage } from './models/mcp';
import { sendResponse, sendError } from './utils/mcp';
import { allTools, allToolDefinitions } from './tools';
/**
* Validate environment variables on startup
*/
function validateEnvironment(): boolean {
const requiredVars = [
'SERVICENOW_INSTANCE_URL',
'SERVICENOW_USERNAME',
'SERVICENOW_PASSWORD'
];
const missing = requiredVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
log(`ERROR: Missing required environment variables: ${missing.join(', ')}`);
log('Please set the following environment variables:');
log('- SERVICENOW_INSTANCE_URL: Your ServiceNow instance URL');
log('- SERVICENOW_USERNAME: ServiceNow username');
log('- SERVICENOW_PASSWORD: ServiceNow password');
log('- SERVICENOW_DEFAULT_SENDER_EMAIL: Default email sender (optional)');
return false;
}
// Validate instance URL format
const instanceUrl = process.env.SERVICENOW_INSTANCE_URL;
if (instanceUrl && !instanceUrl.startsWith('https://')) {
log('WARNING: SERVICENOW_INSTANCE_URL should start with https://');
}
log('Environment variables validated successfully');
return true;
}
/**
* Process incoming JSON-RPC message
*/
async function processMessage(message: JsonRpcMessage): Promise<void> {
log(`Processing message: ${JSON.stringify(message)}`);
if (typeof message.id === 'undefined' || message.id === null) {
log(`Received notification: ${message.method}`);
return;
}
log(`Processing method: ${message.method} with id: ${message.id}`);
try {
switch (message.method) {
case 'initialize':
log('HANDLING INITIALIZE REQUEST');
sendResponse(message.id, {
protocolVersion: message.params?.protocolVersion || '2024-11-05',
capabilities: {
tools: {},
prompts: {},
resources: {}
},
serverInfo: {
name: 'servicenow-mcp',
version: '1.0.0'
}
});
break;
case 'prompts/list':
log('HANDLING PROMPTS/LIST REQUEST');
sendResponse(message.id, {
prompts: []
});
break;
case 'resources/list':
log('HANDLING RESOURCES/LIST REQUEST');
sendResponse(message.id, {
resources: []
});
break;
case 'tools/list':
log('HANDLING TOOLS/LIST REQUEST');
log(`Available tools: ${allToolDefinitions.map(t => t.name).join(', ')}`);
sendResponse(message.id, {
tools: allToolDefinitions
});
break;
case 'tools/call': // Alias for tools/execute
case 'tools/execute':
const toolName = message.params?.name;
if (!toolName) {
log('ERROR: No tool name provided');
sendError(message.id, -32602, 'Missing required parameter: name');
break;
}
log(`HANDLING TOOLS/EXECUTE REQUEST: ${toolName}`);
log(`Raw message.params: ${JSON.stringify(message.params)}`);
// Try different parameter locations with priority order
let toolParams = {};
if (message.params?.arguments) {
toolParams = message.params.arguments;
log(`Using arguments: ${JSON.stringify(toolParams)}`);
} else if (message.params?.parameters) {
toolParams = message.params.parameters;
log(`Using parameters: ${JSON.stringify(toolParams)}`);
} else {
// Sometimes parameters are directly in the params object
const { name, ...otherParams } = message.params || {};
if (Object.keys(otherParams).length > 0) {
toolParams = otherParams;
log(`Using direct params: ${JSON.stringify(toolParams)}`);
} else {
toolParams = {};
log(`No parameters found, using empty object`);
}
}
log(`Final tool parameters: ${JSON.stringify(toolParams)}`);
const tool = allTools[toolName];
if (tool) {
try {
log(`Executing tool: ${toolName}`);
const startTime = Date.now();
const result = await tool.execute(toolParams);
const endTime = Date.now();
log(`Tool execution completed in ${endTime - startTime}ms`);
log(`Tool execution result: ${JSON.stringify(result)}`);
// Validate result format
if (!result || typeof result !== 'object') {
log(`WARNING: Tool ${toolName} returned invalid result format`);
sendError(message.id, -32603, `Tool ${toolName} returned invalid result`);
} else if (!result.content || !Array.isArray(result.content)) {
log(`WARNING: Tool ${toolName} returned result without proper content array`);
sendError(message.id, -32603, `Tool ${toolName} returned malformed result`);
} else {
sendResponse(message.id, result);
}
} catch (toolError: any) {
log(`Tool execution error: ${toolError.message}`);
log(`Tool error stack: ${toolError.stack}`);
// Provide more specific error messages
let errorMessage = `Tool execution error: ${toolError.message}`;
if (toolError.code === 'ECONNREFUSED') {
errorMessage = 'Cannot connect to ServiceNow instance. Please check SERVICENOW_INSTANCE_URL.';
} else if (toolError.response?.status === 401) {
errorMessage = 'Authentication failed. Please check ServiceNow credentials.';
} else if (toolError.response?.status === 403) {
errorMessage = 'Permission denied. Please check ServiceNow user roles.';
} else if (toolError.response?.status === 404) {
errorMessage = 'ServiceNow endpoint not found. Please check instance URL and API availability.';
}
sendError(message.id, -32603, errorMessage);
}
} else {
log(`Tool not found: ${toolName}`);
log(`Available tools: ${Object.keys(allTools).join(', ')}`);
sendError(message.id, -32601, `Tool not found: ${toolName}. Available tools: ${Object.keys(allTools).join(', ')}`);
}
break;
default:
log(`Unknown method: ${message.method}`);
sendError(message.id, -32601, `Method not found: ${message.method}`);
}
} catch (error: any) {
log(`Error processing message: ${error.message}`);
log(`Error stack: ${error.stack}`);
// More specific error handling
let errorMessage = `Internal error: ${error.message}`;
if (error instanceof SyntaxError) {
errorMessage = 'Invalid JSON in request';
} else if (error.code === 'ENOTFOUND') {
errorMessage = 'Network error: Cannot resolve ServiceNow hostname';
}
sendError(message.id, -32603, errorMessage);
}
}
/**
* Main message processing loop
*/
export function startServer(): void {
log('Server started. Validating environment...');
// Validate environment before starting
if (!validateEnvironment()) {
log('Server startup failed due to missing environment variables');
process.exit(1);
}
log(`Loaded ${Object.keys(allTools).length} tools: ${Object.keys(allTools).join(', ')}`);
log('Waiting for messages...');
let buffer = '';
process.stdin.on('data', (chunk) => {
buffer += chunk.toString();
let newlineIndex;
while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
const line = buffer.slice(0, newlineIndex);
buffer = buffer.slice(newlineIndex + 1);
if (line.trim()) {
try {
const message: JsonRpcMessage = JSON.parse(line);
processMessage(message);
} catch (error: any) {
log(`Error parsing JSON: ${error.message}`);
log(`Problematic line: ${line}`);
// Don't crash the server on JSON parse errors
}
}
}
});
process.stdin.on('end', () => {
log('Input stream ended.');
});
process.stdin.on('error', (error) => {
log(`Stdin error: ${error.message}`);
});
process.on('uncaughtException', (err: Error) => {
log(`Uncaught exception: ${err.message}`);
log(`Stack trace: ${err.stack}`);
// Don't exit immediately, log and continue
});
process.on('unhandledRejection', (reason, promise) => {
log(`Unhandled rejection at: ${promise}, reason: ${reason}`);
// Don't exit immediately, log and continue
});
process.on('SIGTERM', () => {
log('Received SIGTERM, shutting down gracefully.');
process.exit(0);
});
process.on('SIGINT', () => {
log('Received SIGINT, shutting down gracefully.');
process.exit(0);
});
log('MCP server ready to receive messages');
}
// Start the server
startServer();