import { describe, it, expect, beforeAll, afterAll, beforeEach } from '@jest/globals';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { join } from 'path';
describe('MCP Server Tests - ServiceNow Background Script Execution', () => {
let client: Client;
beforeAll(async () => {
// Set up environment variables for ServiceNow (using placeholder values for tests)
process.env.SERVICENOW_ACE_INSTANCE = 'test-instance.service-now.com';
process.env.SERVICENOW_ACE_USERNAME = 'test_user';
process.env.SERVICENOW_ACE_PASSWORD = 'test_password';
// Create MCP client using the official SDK approach
const serverPath = join(__dirname, '../../build/index.js');
const transport = new StdioClientTransport({
command: 'node',
args: [serverPath],
env: {
NODE_ENV: 'test',
SERVICENOW_ACE_INSTANCE: 'test-instance.service-now.com',
SERVICENOW_ACE_USERNAME: 'test_user',
SERVICENOW_ACE_PASSWORD: 'test_password',
},
});
client = new Client({
name: 'test-client',
version: '1.1.0',
});
await client.connect(transport);
}, 10000); // 10 second timeout for server startup
afterAll(async () => {
if (client) {
await client.close();
}
});
describe('Server Connection', () => {
it('should connect to server successfully', () => {
expect(client).toBeDefined();
});
});
describe('Tool Discovery', () => {
it('should list available tools', async () => {
const tools = await client.listTools();
expect(tools).toHaveProperty('tools');
expect(Array.isArray(tools.tools)).toBe(true);
expect(tools.tools.length).toBeGreaterThan(0);
});
it('should expose execute_background_script tool', async () => {
const tools = await client.listTools();
const scriptTool = tools.tools.find((tool: any) => tool.name === 'execute_background_script');
expect(scriptTool).toBeDefined();
expect(scriptTool?.name).toBe('execute_background_script');
expect(scriptTool?.description).toContain('ServiceNow');
expect(scriptTool?.description).toContain('SANDBOX ONLY');
expect(scriptTool?.inputSchema).toBeDefined();
expect(scriptTool?.inputSchema?.properties?.script).toBeDefined();
expect(scriptTool?.inputSchema?.properties?.scope).toBeDefined();
});
it('should have correct tool schema', async () => {
const tools = await client.listTools();
const scriptTool = tools.tools.find((tool: any) => tool.name === 'execute_background_script');
expect((scriptTool as any)?.inputSchema?.required).toContain('script');
expect((scriptTool as any)?.inputSchema?.required).toContain('scope');
expect((scriptTool as any)?.inputSchema?.properties?.script?.type).toBe('string');
expect((scriptTool as any)?.inputSchema?.properties?.scope?.type).toBe('string');
expect((scriptTool as any)?.inputSchema?.properties?.timeout_ms?.type).toBe('number');
});
});
describe('Tool Execution - Error Handling', () => {
it('should handle missing script parameter', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
scope: 'global',
},
});
expect(result.isError).toBe(true);
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
expect(textContent?.text).toBeDefined();
const errorData = JSON.parse(textContent?.text || '{}');
expect(errorData.success).toBe(false);
expect(errorData.error?.code).toBe('MISSING_PARAMETER');
expect(errorData.error?.message).toContain('script');
});
it('should handle missing scope parameter', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("test");',
},
});
expect(result.isError).toBe(true);
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
expect(textContent?.text).toBeDefined();
const errorData = JSON.parse(textContent?.text || '{}');
expect(errorData.success).toBe(false);
expect(errorData.error?.code).toBe('MISSING_PARAMETER');
expect(errorData.error?.message).toContain('scope');
});
it('should handle empty script parameter', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '',
scope: 'global',
},
});
expect(result.isError).toBe(true);
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
expect(textContent?.text).toBeDefined();
const errorData = JSON.parse(textContent?.text || '{}');
expect(errorData.success).toBe(false);
expect(errorData.error?.code).toBe('MISSING_PARAMETER');
});
it('should handle empty scope parameter', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("test");',
scope: '',
},
});
expect(result.isError).toBe(true);
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
expect(textContent?.text).toBeDefined();
const errorData = JSON.parse(textContent?.text || '{}');
expect(errorData.success).toBe(false);
expect(errorData.error?.code).toBe('MISSING_PARAMETER');
});
it('should handle invalid timeout parameter', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("test");',
scope: 'global',
timeout_ms: 500, // Too low
},
});
expect(result.isError).toBe(true);
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
expect(textContent?.text).toBeDefined();
const errorData = JSON.parse(textContent?.text || '{}');
expect(errorData.success).toBe(false);
expect(errorData.error?.code).toBe('VALIDATION_ERROR');
});
it('should handle network errors gracefully', async () => {
// Try with an invalid/unreachable instance
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("test");',
scope: 'global',
},
});
// The call should return without throwing, but indicate an error
expect(result.content).toBeDefined();
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
const responseData = JSON.parse(textContent?.text || '{}');
// Could be various errors depending on test environment
expect(responseData.success === false || responseData.error).toBeDefined();
}, 15000);
it('should handle unknown tools gracefully', async () => {
await expect(
client.callTool({
name: 'unknown_tool',
arguments: {},
})
).rejects.toThrow();
});
});
describe('Response Format', () => {
it('should return JSON response', async () => {
// Note: This test demonstrates the response format structure
// In a real scenario, you'd mock the ServiceNow API
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("test");',
scope: 'global',
},
});
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
expect(textContent).toBeDefined();
// Should be valid JSON (either success or error response)
expect(() => JSON.parse(textContent?.text || '{}')).not.toThrow();
}, 15000);
it('should include required fields in error responses', async () => {
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: '',
scope: 'global',
},
});
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
const errorResponse = JSON.parse(textContent?.text || '{}');
expect(errorResponse).toHaveProperty('success');
expect(errorResponse).toHaveProperty('error');
expect(errorResponse.error).toHaveProperty('code');
expect(errorResponse.error).toHaveProperty('message');
});
});
describe('Tool Parameters', () => {
it('should accept script and scope as string parameters', async () => {
// This test verifies the tool accepts the parameters correctly
// The actual result depends on the ServiceNow instance availability
expect(async () => {
await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("Hello World");',
scope: 'global',
},
});
}).toBeDefined();
});
it('should handle optional timeout_ms parameter', async () => {
expect(async () => {
await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("Hello World");',
scope: 'global',
timeout_ms: 30000,
},
});
}).toBeDefined();
});
it('should validate script length limits', async () => {
// Create a script that's too long (over 50,000 characters)
const longScript = 'gs.print("test");'.repeat(5000); // ~75,000 characters
const result = await client.callTool({
name: 'execute_background_script',
arguments: {
script: longScript,
scope: 'global',
},
});
expect(result.isError).toBe(true);
const textContent = (result.content as any[]).find((c: any) => c?.type === 'text');
const errorData = JSON.parse(textContent?.text || '{}');
expect(errorData.error?.code).toBe('INVALID_SCRIPT');
expect(errorData.error?.message).toContain('length');
});
});
describe('Security Warnings', () => {
it('should include security warnings in tool description', async () => {
const tools = await client.listTools();
const scriptTool = tools.tools.find((tool: any) => tool.name === 'execute_background_script');
expect(scriptTool?.description).toContain('SANDBOX ONLY');
expect(scriptTool?.description).toContain('arbitrary code');
});
});
describe('Global Context Overflow Prevention', () => {
it('should include context protection in background script tool description', async () => {
const tools = await client.listTools();
const scriptTool = tools.tools.find((tool: any) => tool.name === 'execute_background_script');
expect(scriptTool?.description).toContain('🛡️');
expect(scriptTool?.description).toContain('Auto-truncates');
});
it('should have concise tool descriptions', async () => {
const tools = await client.listTools();
const scriptTool = tools.tools.find((tool: any) => tool.name === 'execute_background_script');
const tableTool = tools.tools.find((tool: any) => tool.name === 'execute_table_operation');
// Descriptions should be concise but informative
expect(scriptTool?.description?.length).toBeLessThan(300);
expect(tableTool?.description?.length).toBeLessThan(300);
// Should still contain key information
expect(scriptTool?.description).toContain('SANDBOX ONLY');
expect(tableTool?.description).toContain('CRUD operations');
});
it('should apply global protection to background script responses', async () => {
// This test verifies that global protection is applied to background script responses
// The actual result depends on the ServiceNow instance availability
expect(async () => {
await client.callTool({
name: 'execute_background_script',
arguments: {
script: 'gs.print("Test output");',
scope: 'global',
},
});
}).toBeDefined();
});
});
});