#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { ServiceNowBackgroundScriptClient } from './servicenow/client.js';
import { ServiceNowScriptError, ServiceNowErrorResponse, ERROR_CODES } from './servicenow/types.js';
import { ServiceNowTableClient } from './servicenow/tableClient.js';
import { ServiceNowTableError, TABLE_ERROR_CODES } from './servicenow/tableTypes.js';
import { ServiceNowUpdateSetClient } from './servicenow/updateSetClient.js';
import { ServiceNowUpdateSetError, UPDATE_SET_ERROR_CODES } from './servicenow/updateSetTypes.js';
import { globalContextOverflowPrevention } from './utils/contextOverflowPrevention.js';
import { resolveFilePlaceholders } from './utils/filePlaceholderResolver.js';
import { FilePlaceholderError, FILE_PLACEHOLDER_ERROR_CODES } from './utils/filePlaceholderTypes.js';
// Create the MCP server
const server = new Server(
{
name: 'skyenet-mcp-ace',
version: '1.4.0',
},
{
capabilities: {
tools: {},
},
}
);
// Initialize ServiceNow clients (will throw if credentials missing)
let client: ServiceNowBackgroundScriptClient | null = null;
let tableClient: ServiceNowTableClient | null = null;
let updateSetClient: ServiceNowUpdateSetClient | null = null;
let initError: Error | null = null;
try {
client = new ServiceNowBackgroundScriptClient();
tableClient = new ServiceNowTableClient();
updateSetClient = new ServiceNowUpdateSetClient();
} catch (error) {
initError = error instanceof Error ? error : new Error('Unknown initialization error');
}
// Register tool handlers
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'execute_background_script') {
try {
// Check if client was initialized successfully
if (!client) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'INITIALIZATION_ERROR',
message: 'ServiceNow client failed to initialize',
details:
initError instanceof Error ? initError.message : 'Unknown error',
},
} as ServiceNowErrorResponse),
},
],
isError: true,
};
}
// Extract parameters from tool arguments
const args = request.params.arguments as Record<string, unknown> | undefined;
const script = args?.script as string | undefined;
const scope = args?.scope as string | undefined;
const timeoutMs = args?.timeout_ms as number | undefined;
const includeHtml = args?.include_html as boolean | undefined;
const responseMode = args?.response_mode as string | undefined;
// Validate required parameters
if (!script) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: ERROR_CODES.MISSING_PARAMETER,
message: 'Required parameter "script" is missing',
details: 'Please provide the JavaScript code to execute',
},
} as ServiceNowErrorResponse),
},
],
isError: true,
};
}
if (!scope) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: ERROR_CODES.MISSING_PARAMETER,
message: 'Required parameter "scope" is missing',
details: 'Please provide the application scope (e.g., "global")',
},
} as ServiceNowErrorResponse),
},
],
isError: true,
};
}
// Resolve file placeholders in the request
let resolvedScript = script;
let resolvedScope = scope;
try {
const scriptResolution = resolveFilePlaceholders(script);
resolvedScript = scriptResolution.data;
if (scope) {
const scopeResolution = resolveFilePlaceholders(scope);
resolvedScope = scopeResolution.data;
}
} catch (error) {
if (error instanceof FilePlaceholderError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'FILE_PLACEHOLDER_ERROR',
message: 'Failed to resolve file placeholder',
details: `${error.placeholder}: ${error.message}`,
},
} as ServiceNowErrorResponse),
},
],
isError: true,
};
}
// Re-throw unknown errors
throw error;
}
// Execute the script
const result = await client.executeScript({
script: resolvedScript,
scope: resolvedScope,
timeoutMs,
include_html: includeHtml,
response_mode: responseMode as any,
});
// Apply global context overflow prevention
const { response: protectedResult, monitoring } = globalContextOverflowPrevention.monitorResponse(result, 'execute_background_script', responseMode);
// Defensive check: ensure protectedResult is never null or undefined
// This prevents bare null from being serialized, which breaks Codex CLI parsing
if (protectedResult == null) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'RESPONSE_SERIALIZATION_ERROR',
message: 'Response serialization failed: result was null',
details: 'The script execution completed but produced a null response. This should not happen.',
},
}),
},
],
isError: true,
};
}
// Ensure output.text is always a string, never null
if (protectedResult.success && protectedResult.output) {
if (protectedResult.output.text == null || typeof protectedResult.output.text !== 'string') {
protectedResult.output.text = '';
}
}
return {
content: [
{
type: 'text',
text: JSON.stringify(protectedResult, null, 2),
},
],
};
} catch (error) {
const errorResponse: ServiceNowErrorResponse = {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: 'An unexpected error occurred',
},
};
if (error instanceof ServiceNowScriptError) {
errorResponse.error.code = error.code;
errorResponse.error.message = error.message;
errorResponse.error.details = `HTTP Status: ${error.statusCode || 'N/A'}`;
} else if (error instanceof Error) {
errorResponse.error.message = error.message;
errorResponse.error.details = 'Check ServiceNow instance connectivity and credentials';
}
return {
content: [
{
type: 'text',
text: JSON.stringify(errorResponse),
},
],
isError: true,
};
}
}
if (request.params.name === 'execute_table_operation') {
try {
// Check if table client was initialized successfully
if (!tableClient) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'INITIALIZATION_ERROR',
message: 'ServiceNow table client failed to initialize',
details:
initError instanceof Error ? initError.message : 'Unknown error',
},
}),
},
],
isError: true,
};
}
// Extract parameters from tool arguments
const args = request.params.arguments as Record<string, unknown> | undefined;
const operation = args?.operation as string | undefined;
const table = args?.table as string | undefined;
const sysId = args?.sys_id as string | undefined;
const sysIds = args?.sys_ids as string[] | undefined;
const query = args?.query as string | undefined;
const fields = args?.fields as string | undefined;
const limit = args?.limit as number | undefined;
const offset = args?.offset as number | undefined;
const displayValue = args?.display_value as string | undefined;
const excludeReferenceLink = args?.exclude_reference_link as boolean | undefined;
const data = args?.data as Record<string, any> | Record<string, any>[] | undefined;
const batch = args?.batch as boolean | undefined;
const validateFields = args?.validate_fields as boolean | undefined;
const contextOverflowPrevention = args?.context_overflow_prevention as boolean | undefined;
const strictFields = args?.strict_fields as boolean | undefined;
const responseMode = args?.response_mode as string | undefined;
// Validate required parameters
if (!operation) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: TABLE_ERROR_CODES.MISSING_PARAMETER,
message: 'Required parameter "operation" is missing',
details: 'Please provide the operation type (GET, POST, PUT, PATCH, DELETE)',
},
}),
},
],
isError: true,
};
}
if (!table) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: TABLE_ERROR_CODES.MISSING_PARAMETER,
message: 'Required parameter "table" is missing',
details: 'Please provide the ServiceNow table name',
},
}),
},
],
isError: true,
};
}
// Configure context overflow prevention if specified
if (contextOverflowPrevention !== undefined) {
tableClient.updateConfig({
enableResultSummarization: contextOverflowPrevention,
});
}
// Resolve file placeholders in the request arguments
let resolvedArgs = {
operation: operation as any,
table,
sys_id: sysId,
sys_ids: sysIds,
query,
fields,
limit,
offset,
display_value: displayValue as any,
exclude_reference_link: excludeReferenceLink,
data,
batch,
validate_fields: validateFields,
strict_fields: strictFields,
response_mode: responseMode as any,
};
try {
const resolution = resolveFilePlaceholders(resolvedArgs);
resolvedArgs = resolution.data;
} catch (error) {
if (error instanceof FilePlaceholderError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'FILE_PLACEHOLDER_ERROR',
message: 'Failed to resolve file placeholder',
details: `${error.placeholder}: ${error.message}`,
},
}),
},
],
isError: true,
};
}
// Re-throw unknown errors
throw error;
}
// Execute the table operation
const result = await tableClient.executeTableOperation({
operation: resolvedArgs.operation,
table: resolvedArgs.table,
sys_id: resolvedArgs.sys_id,
sys_ids: resolvedArgs.sys_ids,
query: resolvedArgs.query,
fields: resolvedArgs.fields,
limit: resolvedArgs.limit,
offset: resolvedArgs.offset,
display_value: resolvedArgs.display_value,
exclude_reference_link: resolvedArgs.exclude_reference_link,
data: resolvedArgs.data,
batch: resolvedArgs.batch,
validate_fields: resolvedArgs.validate_fields,
strict_fields: resolvedArgs.strict_fields,
response_mode: responseMode as any,
});
// Apply global context overflow prevention (additional layer of protection)
const { response: protectedResult, monitoring } = globalContextOverflowPrevention.monitorResponse(result, 'execute_table_operation', responseMode);
return {
content: [
{
type: 'text',
text: JSON.stringify(protectedResult, null, 2),
},
],
};
} catch (error) {
const errorResponse = {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: 'An unexpected error occurred',
details: undefined as string | undefined,
},
};
if (error instanceof ServiceNowTableError) {
errorResponse.error.code = error.code;
errorResponse.error.message = error.message;
errorResponse.error.details = `HTTP Status: ${error.statusCode || 'N/A'}`;
} else if (error instanceof Error) {
errorResponse.error.message = error.message;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(errorResponse),
},
],
isError: true,
};
}
}
if (request.params.name === 'execute_updateset_operation') {
try {
// Check if update set client was initialized successfully
if (!updateSetClient) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'INITIALIZATION_ERROR',
message: 'ServiceNow update set client failed to initialize',
details:
initError instanceof Error ? initError.message : 'Unknown error',
},
}),
},
],
isError: true,
};
}
// Extract parameters from tool arguments
const args = request.params.arguments as Record<string, unknown> | undefined;
const operation = args?.operation as string | undefined;
const name = args?.name as string | undefined;
const description = args?.description as string | undefined;
const scope = args?.scope as string | undefined;
const setAsWorking = args?.set_as_working as boolean | undefined;
const updateSetSysId = args?.update_set_sys_id as string | undefined;
const table = args?.table as string | undefined;
const sysId = args?.sys_id as string | undefined;
const data = args?.data as Record<string, any> | Record<string, any>[] | undefined;
const batch = args?.batch as boolean | undefined;
const xmlSysIds = args?.xml_sys_ids as string[] | undefined;
const query = args?.query as string | undefined;
const force = args?.force as boolean | undefined;
const limit = args?.limit as number | undefined;
const offset = args?.offset as number | undefined;
const filters = args?.filters as Record<string, any> | undefined;
const responseMode = args?.response_mode as string | undefined;
const quiet = args?.quiet as boolean | undefined;
// Validate required parameters
if (!operation) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: UPDATE_SET_ERROR_CODES.MISSING_PARAMETER,
message: 'Required parameter "operation" is missing',
details: 'Please provide the operation type (create, set_working, show_working, clear_working, insert, update, rehome, contents, recent, list, info, complete, reopen, delete, diff_default)',
},
}),
},
],
isError: true,
};
}
// Resolve file placeholders in the request arguments
let resolvedArgs = {
operation: operation as any,
name,
description,
scope,
set_as_working: setAsWorking,
update_set_sys_id: updateSetSysId,
table,
sys_id: sysId,
data,
batch,
xml_sys_ids: xmlSysIds,
query,
force,
limit,
offset,
filters,
response_mode: responseMode as any,
quiet,
};
try {
const resolution = resolveFilePlaceholders(resolvedArgs);
resolvedArgs = resolution.data;
} catch (error) {
if (error instanceof FilePlaceholderError) {
return {
content: [
{
type: 'text',
text: JSON.stringify({
success: false,
error: {
code: 'FILE_PLACEHOLDER_ERROR',
message: 'Failed to resolve file placeholder',
details: `${error.placeholder}: ${error.message}`,
},
}),
},
],
isError: true,
};
}
// Re-throw unknown errors
throw error;
}
// Set timestamp after file placeholder resolution for accurate XML detection
// Subtract 500ms to account for timing between placeholder resolution and record creation
const timestampBefore = Date.now() - 500;
// Execute the update set operation
const result = await updateSetClient.executeUpdateSetOperation({
operation: resolvedArgs.operation,
name: resolvedArgs.name,
description: resolvedArgs.description,
scope: resolvedArgs.scope,
set_as_working: resolvedArgs.set_as_working,
update_set_sys_id: resolvedArgs.update_set_sys_id,
table: resolvedArgs.table,
sys_id: resolvedArgs.sys_id,
data: resolvedArgs.data,
batch: resolvedArgs.batch,
xml_sys_ids: resolvedArgs.xml_sys_ids,
query: resolvedArgs.query,
force: resolvedArgs.force,
limit: resolvedArgs.limit,
offset: resolvedArgs.offset,
filters: resolvedArgs.filters,
custom_timestamp_before: timestampBefore,
response_mode: responseMode as any,
quiet: resolvedArgs.quiet,
});
// Apply global context overflow prevention
const { response: protectedResult, monitoring } = globalContextOverflowPrevention.monitorResponse(result, 'execute_updateset_operation', responseMode);
return {
content: [
{
type: 'text',
text: JSON.stringify(protectedResult, null, 2),
},
],
};
} catch (error) {
const errorResponse = {
success: false,
error: {
code: 'UNKNOWN_ERROR',
message: 'An unexpected error occurred',
details: undefined as string | undefined,
},
};
if (error instanceof ServiceNowUpdateSetError) {
errorResponse.error.code = error.code;
errorResponse.error.message = error.message;
errorResponse.error.details = `HTTP Status: ${error.statusCode || 'N/A'}`;
} else if (error instanceof Error) {
errorResponse.error.message = error.message;
}
return {
content: [
{
type: 'text',
text: JSON.stringify(errorResponse),
},
],
isError: true,
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'execute_background_script',
description:
'Execute server-side JavaScript in ServiceNow using Background Scripts. ⚠️ SANDBOX ONLY - executes arbitrary code. 🛡️ Auto-truncates large outputs. 📁 Use {{file:path}} for large scripts.',
inputSchema: {
type: 'object',
properties: {
script: {
type: 'string',
description:
'The server-side JavaScript code to execute (e.g., gs.print("Hello");). Maximum 50,000 characters. Supports {{file:...}} placeholders to load content from local files.',
},
scope: {
type: 'string',
description:
'The application scope (e.g., "global" or specific app scope). Required.',
},
timeout_ms: {
type: 'number',
description:
'Optional timeout in milliseconds (default: 60000, range: 1000-300000)',
},
include_html: {
type: 'boolean',
description:
'Include HTML output in response (default: true). Set to false for text-only mode to reduce response size.',
},
response_mode: {
type: 'string',
enum: ['full', 'minimal', 'compact'],
description:
'Response verbosity: full (all data), minimal (essential only), compact (summarized). Default: full',
},
},
required: ['script', 'scope'],
},
},
{
name: 'execute_table_operation',
description:
'CRUD operations on ServiceNow tables via Table API. Supports GET/POST/PUT/PATCH/DELETE with query syntax and batch operations. ⚠️ SANDBOX ONLY - reads/modifies data. 🛡️ Auto-limits large results. Use pagination for big datasets. 📁 Use {{file:path}} for large data.',
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
description: 'The operation to perform on the table. Required.',
},
table: {
type: 'string',
description: 'The ServiceNow table name (e.g., "incident", "sys_user"). Required.',
},
sys_id: {
type: 'string',
description: 'System ID for single record operations (GET, PUT, PATCH, DELETE).',
},
sys_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of system IDs for batch operations.',
},
query: {
type: 'string',
description: 'ServiceNow encoded query string (e.g., "active=true^priority=1").',
},
fields: {
type: 'string',
description: 'Comma-separated list of fields to return.',
},
limit: {
type: 'number',
description: 'Maximum number of records to return (default: 1000).',
},
offset: {
type: 'number',
description: 'Number of records to skip for pagination.',
},
display_value: {
type: 'string',
enum: ['true', 'false', 'all'],
description: 'Return display values for reference fields.',
},
exclude_reference_link: {
type: 'boolean',
description: 'Exclude reference link fields from response.',
},
data: {
type: 'object',
description: 'Record data for POST/PUT/PATCH operations. Can be single object or array for batch operations. Supports {{file:...}} placeholders to load content from local files.',
},
batch: {
type: 'boolean',
description: 'Enable batch mode for multiple record operations.',
},
validate_fields: {
type: 'boolean',
description: 'Enable field validation warnings to catch typos and invalid field names. Default: true (validation enabled by default).',
},
context_overflow_prevention: {
type: 'boolean',
description: 'Enable context overflow prevention to limit large result sets. Default: true. Set to false to disable automatic truncation (use with caution).',
},
strict_fields: {
type: 'boolean',
description: 'Strict field filtering - only return requested fields and strip large fields (script, html, css) unless explicitly requested. Default: false.',
},
response_mode: {
type: 'string',
enum: ['full', 'minimal', 'compact'],
description: 'Response verbosity: full (all data), minimal (essential only), compact (summarized). Default: full',
},
},
required: ['operation', 'table'],
},
},
{
name: 'execute_updateset_operation',
description:
'Manage ServiceNow update sets with lifecycle operations, XML reassignment, and working set tracking. ⚠️ SANDBOX ONLY - modifies update sets. 🛡️ Auto-limits large results. Use pagination for big datasets. 📁 Use {{file:path}} for large data.',
inputSchema: {
type: 'object',
properties: {
operation: {
type: 'string',
enum: ['create', 'set_working', 'show_working', 'clear_working', 'insert', 'update', 'rehome', 'contents', 'recent', 'list', 'info', 'complete', 'reopen', 'delete', 'diff_default'],
description: 'The update set operation to perform. Required.',
},
name: {
type: 'string',
description: 'Update set name (required for create operation).',
},
description: {
type: 'string',
description: 'Update set description (optional for create operation).',
},
scope: {
type: 'string',
description: 'Update set scope (optional, defaults to configured scope).',
},
set_as_working: {
type: 'boolean',
description: 'Set the created update set as working set (for create operation).',
},
update_set_sys_id: {
type: 'string',
description: 'Update set sys_id for operations that require it.',
},
table: {
type: 'string',
description: 'Table name for insert/update operations.',
},
sys_id: {
type: 'string',
description: 'Record sys_id for update operations.',
},
data: {
type: 'object',
description: 'Record data for insert/update operations. Can be single object or array for batch operations. Supports {{file:...}} placeholders to load content from local files.',
},
batch: {
type: 'boolean',
description: 'Enable batch mode for multiple record operations.',
},
xml_sys_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of XML sys_ids for rehome operations.',
},
query: {
type: 'string',
description: 'ServiceNow encoded query string for rehome operations.',
},
force: {
type: 'boolean',
description: 'Force reassignment even if XML is not in Default update set.',
},
limit: {
type: 'number',
description: 'Maximum number of records to return for list/recent operations.',
},
offset: {
type: 'number',
description: 'Number of records to skip for pagination.',
},
filters: {
type: 'object',
description: 'Filters for list operations (scope, state, created_by, sys_created_on).',
},
response_mode: {
type: 'string',
enum: ['full', 'minimal', 'compact'],
description: 'Response verbosity: full (all data), minimal (essential only), compact (summarized). Default: full',
},
quiet: {
type: 'boolean',
description: 'Compact acknowledgment for update operations to avoid RESPONSE_TOO_LARGE errors. Default: false.',
},
},
required: ['operation'],
},
},
],
};
});
// Start the server with stdio transport
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('SkyeNet MCP ACE Server (ServiceNow Background Scripts) started and ready for stdio communication');
}
// Handle process termination gracefully
process.on('SIGINT', () => {
console.error('Shutting down MCP server...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('Shutting down MCP server...');
process.exit(0);
});
// Start the server
main().catch((error) => {
console.error('Failed to start MCP server:', error);
process.exit(1);
});