Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
xcodebuild-validate-capabilities.test.ts9.63 kB
import { validateCapabilitiesTool } from '../../../src/tools/xcodebuild/validate-capabilities.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; // Mock build settings cache jest.mock('../../../src/state/build-settings-cache.js', () => ({ buildSettingsCache: { getBuildSettings: jest.fn().mockResolvedValue({ NSCameraUsageDescription: 'This app needs camera access for photos', NSLocationWhenInUseUsageDescription: 'This app needs location for maps', NSPhotoLibraryUsageDescription: 'This app needs photo library access', }), }, })); describe('validateCapabilitiesTool', () => { const validProjectPath = '/path/to/MyApp.xcworkspace'; const validScheme = 'MyApp'; const validUDID = 'device-iphone16pro'; beforeEach(() => { jest.clearAllMocks(); }); describe('successful capability validation', () => { it('should validate app capabilities', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.isError).toBe(false); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); expect(response.capabilities).toBeDefined(); }); it('should extract required capabilities from build settings', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.capabilities.required).toBeDefined(); expect(Array.isArray(response.capabilities.required)).toBe(true); expect(response.capabilities.required.length).toBe(3); // Check extracted capabilities const capabilities = response.capabilities.required; expect(capabilities.find((c: any) => c.service === 'camera')).toBeDefined(); expect(capabilities.find((c: any) => c.service === 'location')).toBeDefined(); expect(capabilities.find((c: any) => c.service === 'photos')).toBeDefined(); }); it('should include capability descriptions', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); const cameraCapability = response.capabilities.required.find( (c: any) => c.service === 'camera' ); expect(cameraCapability).toBeDefined(); expect(cameraCapability.description).toBe('Camera access'); expect(cameraCapability.infoPlistKey).toBe('NSCameraUsageDescription'); }); it('should include capabilities count', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.capabilities.count).toBe(3); }); it('should provide guidance with capability summary', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.guidance).toBeDefined(); expect(Array.isArray(response.guidance)).toBe(true); expect(response.guidance.length).toBeGreaterThan(0); // Should include count expect(response.guidance.some((g: string) => g.includes('3 required capabilities'))).toBe( true ); }); it('should provide permission grant commands when udid specified', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, udid: validUDID, }); const response = JSON.parse(result.content[0].text); // Should include simctl-privacy commands expect(response.guidance.some((g: string) => g.includes('To grant permissions'))).toBe(true); expect(response.guidance.some((g: string) => g.includes('simctl-privacy'))).toBe(true); expect(response.guidance.some((g: string) => g.includes(validUDID))).toBe(true); expect(response.guidance.some((g: string) => g.includes('service: "camera"'))).toBe(true); }); it('should prompt for udid when not specified', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.guidance.some((g: string) => g.includes('Specify udid parameter'))).toBe( true ); }); it('should handle projects with no capabilities', async () => { const { buildSettingsCache } = await import('../../../src/state/build-settings-cache.js'); (buildSettingsCache.getBuildSettings as jest.Mock).mockResolvedValueOnce({}); const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.capabilities.required.length).toBe(0); expect(response.capabilities.count).toBe(0); }); it('should handle all capability types', async () => { const { buildSettingsCache } = await import('../../../src/state/build-settings-cache.js'); (buildSettingsCache.getBuildSettings as jest.Mock).mockResolvedValueOnce({ NSCameraUsageDescription: 'Camera', NSMicrophoneUsageDescription: 'Microphone', NSLocationWhenInUseUsageDescription: 'Location when in use', NSLocationAlwaysAndWhenInUseUsageDescription: 'Location always', NSContactsUsageDescription: 'Contacts', NSPhotoLibraryUsageDescription: 'Photos', NSCalendarsUsageDescription: 'Calendar', NSHealthShareUsageDescription: 'Health', NSRemindersUsageDescription: 'Reminders', NSMotionUsageDescription: 'Motion', }); const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); // Should have all 10 capabilities (note: location appears twice but maps to same service) expect(response.capabilities.required.length).toBe(10); const services = response.capabilities.required.map((c: any) => c.service); expect(services).toContain('camera'); expect(services).toContain('microphone'); expect(services).toContain('location'); expect(services).toContain('contacts'); expect(services).toContain('photos'); expect(services).toContain('calendar'); expect(services).toContain('health'); expect(services).toContain('reminders'); expect(services).toContain('motion'); }); }); describe('input validation', () => { it('should reject missing projectPath', async () => { await expect( validateCapabilitiesTool({ scheme: validScheme, }) ).rejects.toThrow(McpError); }); it('should reject missing scheme', async () => { await expect( validateCapabilitiesTool({ projectPath: validProjectPath, }) ).rejects.toThrow(McpError); }); it('should reject empty projectPath', async () => { await expect( validateCapabilitiesTool({ projectPath: '', scheme: validScheme, }) ).rejects.toThrow(McpError); }); it('should reject empty scheme', async () => { await expect( validateCapabilitiesTool({ projectPath: validProjectPath, scheme: '', }) ).rejects.toThrow(McpError); }); }); describe('error handling', () => { it('should handle build settings cache errors', async () => { const { buildSettingsCache } = await import('../../../src/state/build-settings-cache.js'); (buildSettingsCache.getBuildSettings as jest.Mock).mockRejectedValueOnce( new Error('Failed to read build settings') ); await expect( validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }) ).rejects.toThrow(McpError); }); }); describe('response format', () => { it('should return proper MCP response format', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.content).toBeDefined(); expect(result.content[0]).toBeDefined(); expect(result.content[0].type).toBe('text'); expect(typeof result.content[0].text).toBe('string'); expect(result.isError).toBe(false); }); it('should return valid JSON in text content', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(() => JSON.parse(result.content[0].text)).not.toThrow(); }); it('should include all required response fields', async () => { const result = await validateCapabilitiesTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response).toHaveProperty('success'); expect(response).toHaveProperty('capabilities'); expect(response).toHaveProperty('guidance'); expect(response.capabilities).toHaveProperty('required'); expect(response.capabilities).toHaveProperty('count'); }); }); });

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

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