Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
command.test.ts10.9 kB
import { jest } from '@jest/globals'; import { exec, execSync } from 'child_process'; import { buildXcodebuildCommand, buildSimctlCommand, executeCommand, executeCommandSync, } from '../../../src/utils/command.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; // Mock child_process jest.mock('child_process', () => ({ exec: jest.fn(), execSync: jest.fn(), })); const mockExec = exec as jest.MockedFunction<typeof exec>; const mockExecSync = execSync as jest.MockedFunction<typeof execSync>; describe('buildXcodebuildCommand', () => { it('should build basic project command', () => { const command = buildXcodebuildCommand('build', '/path/to/Project.xcodeproj'); expect(command).toBe('xcodebuild -project "/path/to/Project.xcodeproj" build'); }); it('should build workspace command', () => { const command = buildXcodebuildCommand('build', '/path/to/Workspace.xcworkspace'); expect(command).toBe('xcodebuild -workspace "/path/to/Workspace.xcworkspace" build'); }); it('should build command with workspace option', () => { const command = buildXcodebuildCommand('build', '/path/to/Project.xcodeproj', { workspace: true, }); expect(command).toBe('xcodebuild -workspace "/path/to/Project.xcodeproj" build'); }); it('should build command with all options', () => { const options = { scheme: 'MyApp', configuration: 'Release', destination: 'platform=iOS Simulator,name=iPhone 15', sdk: 'iphonesimulator', derivedDataPath: '/tmp/DerivedData', json: true, }; const command = buildXcodebuildCommand('build', '/path/to/Project.xcodeproj', options); expect(command).toContain('-project "/path/to/Project.xcodeproj"'); expect(command).toContain('-scheme "MyApp"'); expect(command).toContain('-configuration Release'); expect(command).toContain('-destination "platform=iOS Simulator,name=iPhone 15"'); expect(command).toContain('-sdk iphonesimulator'); expect(command).toContain('-derivedDataPath "/tmp/DerivedData"'); expect(command).toContain('-json'); expect(command).toContain('build'); }); it('should handle scheme with spaces', () => { const command = buildXcodebuildCommand('build', '/path/to/Project.xcodeproj', { scheme: 'My App Scheme', }); expect(command).toContain('-scheme "My App Scheme"'); }); it('should handle empty action', () => { const command = buildXcodebuildCommand('', '/path/to/Project.xcodeproj'); expect(command).toBe('xcodebuild -project "/path/to/Project.xcodeproj"'); }); it('should handle partial options', () => { const command = buildXcodebuildCommand('clean', '/path/to/Project.xcodeproj', { scheme: 'MyApp', configuration: 'Debug', }); expect(command).toContain('-scheme "MyApp"'); expect(command).toContain('-configuration Debug'); expect(command).not.toContain('-destination'); expect(command).not.toContain('-json'); }); }); describe('buildSimctlCommand', () => { it('should build basic list command', () => { const command = buildSimctlCommand('list'); expect(command).toBe('xcrun simctl list'); }); it('should build list command with JSON flag', () => { const command = buildSimctlCommand('list', { json: true }); expect(command).toBe('xcrun simctl list -j'); }); it('should build boot command with device ID', () => { const command = buildSimctlCommand('boot', { deviceId: '12345678-1234-1234-1234-123456789012', }); expect(command).toBe('xcrun simctl boot 12345678-1234-1234-1234-123456789012'); }); it('should build shutdown command with device ID', () => { const command = buildSimctlCommand('shutdown', { deviceId: '12345678-1234-1234-1234-123456789012', }); expect(command).toBe('xcrun simctl shutdown 12345678-1234-1234-1234-123456789012'); }); it('should build create command with full parameters', () => { const command = buildSimctlCommand('create', { name: 'Test iPhone', deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15', runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-17-0', }); expect(command).toBe( 'xcrun simctl create "Test iPhone" com.apple.CoreSimulator.SimDeviceType.iPhone-15 com.apple.CoreSimulator.SimRuntime.iOS-17-0' ); }); it('should not add JSON flag for unsupported actions', () => { const command = buildSimctlCommand('boot', { deviceId: '12345678-1234-1234-1234-123456789012', json: true, }); expect(command).toBe('xcrun simctl boot 12345678-1234-1234-1234-123456789012'); expect(command).not.toContain('-j'); }); it('should handle actions without device-specific parameters', () => { const command = buildSimctlCommand('help'); expect(command).toBe('xcrun simctl help'); }); it('should ignore deviceId for unsupported actions', () => { const command = buildSimctlCommand('list', { deviceId: '12345678-1234-1234-1234-123456789012', }); expect(command).toBe('xcrun simctl list'); }); it('should handle create command with missing parameters', () => { const command = buildSimctlCommand('create', { name: 'Test iPhone', // Missing deviceType and runtime }); expect(command).toBe('xcrun simctl create'); }); it('should handle device name with spaces in create command', () => { const command = buildSimctlCommand('create', { name: 'My Test Device', deviceType: 'iPhone-15', runtime: 'iOS-17-0', }); expect(command).toContain('"My Test Device"'); }); }); describe('executeCommand', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should execute command successfully', async () => { const mockCallback = jest.fn().mockImplementation((...args: any[]) => { const callback = args[2]; callback(null, { stdout: 'success output', stderr: 'warning message' }); }); mockExec.mockImplementation(mockCallback as any); const result = await executeCommand('echo "test"'); expect(result).toEqual({ stdout: 'success output', stderr: 'warning message', code: 0, }); expect(mockExec).toHaveBeenCalledWith( 'echo "test"', expect.objectContaining({ timeout: 300000, maxBuffer: 10 * 1024 * 1024, }), expect.any(Function) ); }); it('should handle command timeout', async () => { const timeoutError = new Error('Command failed') as any; timeoutError.code = 'ETIMEDOUT'; const mockCallback = jest.fn().mockImplementation((...args: any[]) => { const callback = args[2]; callback(timeoutError); }); mockExec.mockImplementation(mockCallback as any); await expect(executeCommand('sleep 10')).rejects.toThrow(McpError); await expect(executeCommand('sleep 10')).rejects.toThrow( 'Command timed out after 300000ms: sleep 10' ); }); it('should handle command failure with stderr', async () => { const execError = new Error('Command failed') as any; execError.code = 1; execError.stdout = 'partial output'; execError.stderr = 'error message'; const mockCallback = jest.fn().mockImplementation((...args: any[]) => { const callback = args[2]; callback(execError); }); mockExec.mockImplementation(mockCallback as any); const result = await executeCommand('false'); expect(result).toEqual({ stdout: 'partial output', stderr: 'error message', code: 1, }); }); it('should handle command failure without stdout/stderr', async () => { const execError = new Error('Permission denied') as any; execError.code = 126; const mockCallback = jest.fn().mockImplementation((...args: any[]) => { const callback = args[2]; callback(execError); }); mockExec.mockImplementation(mockCallback as any); const result = await executeCommand('invalid-command'); expect(result).toEqual({ stdout: '', stderr: 'Permission denied', code: 126, }); }); it('should use custom options', async () => { const mockCallback = jest.fn().mockImplementation((...args: any[]) => { const callback = args[2]; callback(null, { stdout: 'output', stderr: '' }); }); mockExec.mockImplementation(mockCallback as any); await executeCommand('echo "test"', { timeout: 60000, maxBuffer: 1024 }); expect(mockExec).toHaveBeenCalledWith( 'echo "test"', expect.objectContaining({ timeout: 60000, maxBuffer: 1024, }), expect.any(Function) ); }); it('should trim stdout and stderr', async () => { const mockCallback = jest.fn().mockImplementation((...args: any[]) => { const callback = args[2]; callback(null, { stdout: ' output with spaces ', stderr: ' warning ' }); }); mockExec.mockImplementation(mockCallback as any); const result = await executeCommand('echo "test"'); expect(result.stdout).toBe('output with spaces'); expect(result.stderr).toBe('warning'); }); }); describe('executeCommandSync', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should execute command successfully', () => { mockExecSync.mockReturnValue('success output '); const result = executeCommandSync('echo "test"'); expect(result).toEqual({ stdout: 'success output', stderr: '', code: 0, }); expect(mockExecSync).toHaveBeenCalledWith('echo "test"', { encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, }); }); it('should handle command failure with stdout/stderr', () => { const execError = new Error('Command failed') as any; execError.status = 1; execError.stdout = 'partial output '; execError.stderr = ' error message'; mockExecSync.mockImplementation(() => { throw execError; }); const result = executeCommandSync('false'); expect(result).toEqual({ stdout: 'partial output', stderr: 'error message', code: 1, }); }); it('should handle command failure without stdout/stderr', () => { const execError = new Error('Permission denied') as any; execError.status = 126; mockExecSync.mockImplementation(() => { throw execError; }); const result = executeCommandSync('invalid-command'); expect(result).toEqual({ stdout: '', stderr: 'Permission denied', code: 126, }); }); it('should handle missing status code', () => { const execError = new Error('Unknown error') as any; // No status property mockExecSync.mockImplementation(() => { throw execError; }); const result = executeCommandSync('unknown-command'); expect(result).toEqual({ stdout: '', stderr: 'Unknown error', code: 1, }); }); });

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/conorluddy/xc-mcp'

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