Skip to main content
Glama
mcp-handlers.test.js9.04 kB
import { jest } from '@jest/globals'; // Mock the MCP SDK const mockServer = { setRequestHandler: jest.fn(), connect: jest.fn() }; jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({ Server: jest.fn().mockImplementation(() => mockServer) })); jest.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({ StdioServerTransport: jest.fn() })); jest.mock('@modelcontextprotocol/sdk/types.js', () => ({ CallToolRequestSchema: 'CallToolRequestSchema', ErrorCode: { MethodNotFound: 'MethodNotFound', InternalError: 'InternalError' }, ListToolsRequestSchema: 'ListToolsRequestSchema', McpError: class McpError extends Error { constructor(code, message) { super(message); this.code = code; } } })); // Mock child_process const mockSpawn = jest.fn(); jest.mock('child_process', () => ({ spawn: mockSpawn })); // Suppress console.error during tests const originalConsoleError = console.error; beforeAll(() => { console.error = jest.fn(); }); afterAll(() => { console.error = originalConsoleError; }); // Skip MCP handler tests when Xcode is not available const describeIfXcode = process.env.SKIP_XCODE_TESTS ? describe.skip : describe; describeIfXcode('MCP Tool Handlers', () => { let XcodeMCPServer; let server; let listToolsHandler; let callToolHandler; beforeAll(async () => { const module = await import('../dist/index.js'); XcodeMCPServer = module.default || module.XcodeMCPServer; }); beforeEach(() => { server = new XcodeMCPServer(); // Extract the handlers that were registered const setRequestHandlerCalls = mockServer.setRequestHandler.mock.calls; const listToolsCall = setRequestHandlerCalls.find(call => call[0] === 'ListToolsRequestSchema'); const callToolCall = setRequestHandlerCalls.find(call => call[0] === 'CallToolRequestSchema'); listToolsHandler = listToolsCall ? listToolsCall[1] : null; callToolHandler = callToolCall ? callToolCall[1] : null; }); afterEach(() => { jest.clearAllMocks(); }); describe('ListTools Handler', () => { test('should return all available tools', async () => { expect(listToolsHandler).toBeDefined(); const result = await listToolsHandler(); expect(result).toHaveProperty('tools'); expect(Array.isArray(result.tools)).toBe(true); expect(result.tools.length).toBeGreaterThan(0); // Check for essential tools const toolNames = result.tools.map(tool => tool.name); expect(toolNames).toContain('xcode_open_project'); expect(toolNames).toContain('xcode_build'); expect(toolNames).toContain('xcode_test'); expect(toolNames).toContain('xcode_build_and_run'); expect(toolNames).toContain('xcode_debug'); }); test('should include proper tool schemas', async () => { const result = await listToolsHandler(); const openProjectTool = result.tools.find(tool => tool.name === 'xcode_open_project'); expect(openProjectTool).toHaveProperty('description'); expect(openProjectTool).toHaveProperty('inputSchema'); expect(openProjectTool.inputSchema).toHaveProperty('properties'); expect(openProjectTool.inputSchema.properties).toHaveProperty('path'); expect(openProjectTool.inputSchema.required).toContain('path'); }); test('should include optional parameter tools', async () => { const result = await listToolsHandler(); const testTool = result.tools.find(tool => tool.name === 'xcode_test'); expect(testTool.inputSchema.properties).toHaveProperty('commandLineArguments'); expect(testTool.inputSchema.properties.commandLineArguments.type).toBe('array'); const debugTool = result.tools.find(tool => tool.name === 'xcode_debug'); expect(debugTool.inputSchema.properties).toHaveProperty('scheme'); expect(debugTool.inputSchema.properties).toHaveProperty('skipBuilding'); }); }); describe('CallTool Handler', () => { test('should handle unknown tool names', async () => { expect(callToolHandler).toBeDefined(); const request = { params: { name: 'unknown_tool', arguments: {} } }; await expect(callToolHandler(request)).rejects.toThrow('Unknown tool: unknown_tool'); }); test('should route to correct methods for known tools', async () => { // Mock the individual methods const mockExecuteJXA = jest.fn().mockResolvedValue('Success'); server.executeJXA = mockExecuteJXA; const toolTests = [ { name: 'xcode_build', args: {} }, { name: 'xcode_clean', args: {} }, { name: 'xcode_stop', args: {} }, { name: 'xcode_get_schemes', args: {} }, { name: 'xcode_get_run_destinations', args: {} }, { name: 'xcode_get_workspace_info', args: {} }, { name: 'xcode_get_projects', args: {} } ]; for (const { name, args } of toolTests) { const request = { params: { name, arguments: args } }; const result = await callToolHandler(request); expect(result).toHaveProperty('content'); expect(Array.isArray(result.content)).toBe(true); expect(result.content[0]).toHaveProperty('type', 'text'); expect(result.content[0]).toHaveProperty('text'); } }); test('should handle tools with parameters correctly', async () => { const mockExecuteJXA = jest.fn().mockResolvedValue('Project opened successfully'); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_open_project', arguments: { path: '/Users/test/TestProject.xcodeproj' } } }; const result = await callToolHandler(request); expect(result.content[0].text).toBe('Project opened successfully'); expect(mockExecuteJXA).toHaveBeenCalled(); }); test('should handle array parameters', async () => { const mockExecuteJXA = jest.fn().mockResolvedValue('Test started. Result ID: test-123'); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_test', arguments: { commandLineArguments: ['--verbose', '--parallel-testing-enabled', 'YES'] } } }; const result = await callToolHandler(request); expect(result.content[0].text).toBe('Test started. Result ID: test-123'); expect(mockExecuteJXA).toHaveBeenCalled(); }); test('should handle optional parameters', async () => { const mockExecuteJXA = jest.fn().mockResolvedValue('Debug started. Result ID: debug-456'); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_debug', arguments: { scheme: 'TestScheme', skipBuilding: true } } }; const result = await callToolHandler(request); expect(result.content[0].text).toBe('Debug started. Result ID: debug-456'); expect(mockExecuteJXA).toHaveBeenCalled(); }); test('should propagate execution errors', async () => { const mockExecuteJXA = jest.fn().mockRejectedValue(new Error('JXA execution failed')); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_build', arguments: {} } }; await expect(callToolHandler(request)).rejects.toThrow('Tool execution failed: JXA execution failed'); }); }); describe('Tool Input Validation', () => { test('should handle missing required parameters gracefully', async () => { const mockExecuteJXA = jest.fn(); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_open_project', arguments: {} // Missing required 'path' parameter } }; // The method should handle undefined gracefully await expect(callToolHandler(request)).rejects.toThrow(); }); test('should handle undefined optional parameters', async () => { const mockExecuteJXA = jest.fn().mockResolvedValue('Test started'); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_test', arguments: { // commandLineArguments is optional and not provided } } }; const result = await callToolHandler(request); expect(result).toBeDefined(); }); test('should handle boolean parameters correctly', async () => { const mockExecuteJXA = jest.fn().mockResolvedValue('Debug started'); server.executeJXA = mockExecuteJXA; const request = { params: { name: 'xcode_debug', arguments: { skipBuilding: false } } }; const result = await callToolHandler(request); expect(result).toBeDefined(); }); }); });

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/lapfelix/XcodeMCP'

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