Skip to main content
Glama
tableClient.test.ts18.8 kB
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 Table API Operations', () => { 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 including table operation tool', async () => { const tools = await client.listTools(); expect(tools).toHaveProperty('tools'); expect(Array.isArray(tools.tools)).toBe(true); expect(tools.tools.length).toBeGreaterThanOrEqual(2); // Should have both tools }); it('should expose execute_table_operation tool', async () => { const tools = await client.listTools(); const tableTool = tools.tools.find((tool: any) => tool.name === 'execute_table_operation'); expect(tableTool).toBeDefined(); expect(tableTool?.name).toBe('execute_table_operation'); expect(tableTool?.description).toContain('ServiceNow'); expect(tableTool?.description).toContain('Table API'); expect(tableTool?.description).toContain('CAUTION'); expect(tableTool?.inputSchema).toBeDefined(); expect(tableTool?.inputSchema?.properties?.operation).toBeDefined(); expect(tableTool?.inputSchema?.properties?.table).toBeDefined(); }); it('should have correct tool schema for table operations', async () => { const tools = await client.listTools(); const tableTool = tools.tools.find((tool: any) => tool.name === 'execute_table_operation'); expect((tableTool as any)?.inputSchema?.required).toContain('operation'); expect((tableTool as any)?.inputSchema?.required).toContain('table'); expect((tableTool as any)?.inputSchema?.properties?.operation?.enum).toEqual(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']); expect((tableTool as any)?.inputSchema?.properties?.table?.type).toBe('string'); expect((tableTool as any)?.inputSchema?.properties?.sys_id?.type).toBe('string'); expect((tableTool as any)?.inputSchema?.properties?.query?.type).toBe('string'); expect((tableTool as any)?.inputSchema?.properties?.data?.type).toBe('object'); }); }); describe('Tool Execution - Error Handling', () => { it('should handle missing operation parameter', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { table: 'incident', }, }); 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('operation'); }); it('should handle missing table parameter', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', }, }); 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('table'); }); it('should handle invalid operation parameter', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'INVALID', table: 'incident', }, }); 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('INVALID_OPERATION'); }); it('should handle empty table parameter', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: '', }, }); 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 missing sys_id for DELETE operation', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'DELETE', table: 'incident', }, }); 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 missing data for POST operation', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'POST', table: 'incident', }, }); 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 network errors gracefully', async () => { // Try with an invalid/unreachable instance const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', }, }); // 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 for table operations', 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_table_operation', arguments: { operation: 'GET', table: 'incident', }, }); 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_table_operation', arguments: { operation: 'GET', table: '', }, }); 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 valid operation and table 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_table_operation', arguments: { operation: 'GET', table: 'incident', }, }); }).toBeDefined(); }); it('should handle query parameters', async () => { expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', query: 'active=true', fields: 'number,short_description', limit: 10, }, }); }).toBeDefined(); }); it('should handle single record operations', async () => { expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', sys_id: 'test-sys-id', }, }); }).toBeDefined(); }); it('should handle batch operations', async () => { expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'POST', table: 'incident', data: [ { short_description: 'Test 1' }, { short_description: 'Test 2' }, ], batch: true, }, }); }).toBeDefined(); }); it('should handle update operations', async () => { expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'PUT', table: 'incident', sys_id: 'test-sys-id', data: { short_description: 'Updated description' }, }, }); }).toBeDefined(); }); it('should handle delete operations', async () => { expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'DELETE', table: 'incident', sys_id: 'test-sys-id', }, }); }).toBeDefined(); }); it('should validate batch size limits', async () => { // Create a large batch that exceeds limits const largeBatch = Array(150).fill({ short_description: 'Test' }); const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'POST', table: 'incident', data: largeBatch, batch: true, }, }); 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('BATCH_SIZE_EXCEEDED'); }); }); describe('Security Warnings', () => { it('should include security warnings in tool description', async () => { const tools = await client.listTools(); const tableTool = tools.tools.find((tool: any) => tool.name === 'execute_table_operation'); expect(tableTool?.description).toContain('CAUTION'); expect(tableTool?.description).toContain('sandbox'); expect(tableTool?.description).toContain('read and modify'); }); }); describe('Context Overflow Prevention', () => { it('should include context overflow prevention in tool description', async () => { const tools = await client.listTools(); const tableTool = tools.tools.find((tool: any) => tool.name === 'execute_table_operation'); expect(tableTool?.description).toContain('CONTEXT PROTECTION'); expect(tableTool?.description).toContain('pagination'); }); it('should support context_overflow_prevention parameter', async () => { const tools = await client.listTools(); const tableTool = tools.tools.find((tool: any) => tool.name === 'execute_table_operation'); const schema = (tableTool as any)?.inputSchema; expect(schema?.properties?.context_overflow_prevention).toBeDefined(); expect(schema?.properties?.context_overflow_prevention?.type).toBe('boolean'); expect(schema?.properties?.context_overflow_prevention?.description).toContain('context overflow'); }); it('should apply default limits to prevent large responses', async () => { // This test verifies that the tool applies default limits // The actual result depends on the ServiceNow instance availability expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'sys_user', // No limit specified - should use default limit }, }); }).toBeDefined(); }); it('should allow disabling context overflow prevention', async () => { // This test verifies that context overflow prevention can be disabled expect(async () => { await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'sys_user', context_overflow_prevention: false, }, }); }).toBeDefined(); }); }); describe('Operation Types', () => { it('should support GET operations', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', }, }); expect(result.content).toBeDefined(); const textContent = (result.content as any[]).find((c: any) => c?.type === 'text'); expect(() => JSON.parse(textContent?.text || '{}')).not.toThrow(); }); it('should support POST operations', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'POST', table: 'incident', data: { short_description: 'Test incident' }, }, }); expect(result.content).toBeDefined(); const textContent = (result.content as any[]).find((c: any) => c?.type === 'text'); expect(() => JSON.parse(textContent?.text || '{}')).not.toThrow(); }); it('should support PUT operations', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'PUT', table: 'incident', sys_id: 'test-sys-id', data: { short_description: 'Updated incident' }, }, }); expect(result.content).toBeDefined(); const textContent = (result.content as any[]).find((c: any) => c?.type === 'text'); expect(() => JSON.parse(textContent?.text || '{}')).not.toThrow(); }); it('should support PATCH operations', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'PATCH', table: 'incident', sys_id: 'test-sys-id', data: { short_description: 'Patched incident' }, }, }); expect(result.content).toBeDefined(); const textContent = (result.content as any[]).find((c: any) => c?.type === 'text'); expect(() => JSON.parse(textContent?.text || '{}')).not.toThrow(); }); it('should support DELETE operations', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'DELETE', table: 'incident', sys_id: 'test-sys-id', }, }); expect(result.content).toBeDefined(); const textContent = (result.content as any[]).find((c: any) => c?.type === 'text'); expect(() => JSON.parse(textContent?.text || '{}')).not.toThrow(); }); }); describe('Query Parameters', () => { it('should handle display_value parameter', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', display_value: 'true', }, }); expect(result.content).toBeDefined(); }); it('should handle exclude_reference_link parameter', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', exclude_reference_link: true, }, }); expect(result.content).toBeDefined(); }); it('should handle pagination parameters', async () => { const result = await client.callTool({ name: 'execute_table_operation', arguments: { operation: 'GET', table: 'incident', limit: 5, offset: 10, }, }); expect(result.content).toBeDefined(); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ClearSkye/SkyeNet-MCP-ACE'

If you have feedback or need assistance with the MCP directory API, please join our Discord server