Skip to main content
Glama

XC-MCP: XCode CLI wrapper

by conorluddy
workflow-build-and-run.test.ts16.4 kB
import { buildAndRunTool } from '../../../src/tools/workflows/build-and-run.js'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; // Mock all the tools used in the workflow jest.mock('../../../src/tools/xcodebuild/build.js', () => ({ xcodebuildBuildTool: jest.fn().mockResolvedValue({ content: [ { type: 'text', text: JSON.stringify({ success: true, summary: { duration: 5000, errorCount: 0 }, }), }, ], }), })); jest.mock('../../../src/tools/simctl/boot.js', () => ({ simctlBootTool: jest.fn().mockResolvedValue({ content: [ { type: 'text', text: JSON.stringify({ success: true }), }, ], }), })); jest.mock('../../../src/tools/simctl/suggest.js', () => ({ simctlSuggestTool: jest.fn().mockResolvedValue({ content: [ { type: 'text', text: JSON.stringify({ suggestions: [ { simulator: { udid: 'device-iphone16pro', name: 'iPhone 16 Pro', }, }, ], }), }, ], }), })); jest.mock('../../../src/tools/simctl/install.js', () => ({ simctlInstallTool: jest.fn().mockResolvedValue({ content: [ { type: 'text', text: JSON.stringify({ success: true, bundleId: 'com.example.MyApp', }), }, ], }), })); jest.mock('../../../src/tools/simctl/launch.js', () => ({ simctlLaunchTool: jest.fn().mockResolvedValue({ content: [ { type: 'text', text: JSON.stringify({ success: true }), }, ], }), })); jest.mock('../../../src/tools/simctl/io.js', () => ({ simctlIoTool: jest.fn().mockResolvedValue({ content: [ { type: 'text', text: JSON.stringify({ success: true, filePath: '/path/to/screenshot.png', }), }, ], }), })); jest.mock('../../../src/utils/build-artifacts.js', () => ({ findBuildArtifacts: jest.fn().mockResolvedValue({ appPath: '/path/to/MyApp.app', bundleIdentifier: 'com.example.MyApp', }), })); describe('buildAndRunTool', () => { const validProjectPath = '/path/to/MyApp.xcworkspace'; const validScheme = 'MyApp'; beforeEach(() => { jest.clearAllMocks(); }); describe('successful workflow execution', () => { it('should complete full build-and-run workflow', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.isError).toBe(false); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); expect(response.workflow).toBeDefined(); }); it('should execute all workflow steps in order', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); const steps = response.workflow.steps.map((s: any) => s.name); expect(steps).toEqual(['build', 'suggest', 'boot', 'install', 'launch']); }); it('should mark all steps as successful', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); const allSuccessful = response.workflow.steps.every((s: any) => s.success); expect(allSuccessful).toBe(true); }); it('should include workflow duration', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.workflow.totalDuration).toBeGreaterThanOrEqual(0); expect(response.summary.totalDuration).toBeGreaterThanOrEqual(0); expect(typeof response.workflow.totalDuration).toBe('number'); }); it('should include simulator information in summary', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.summary.simulator).toBeDefined(); expect(response.summary.simulator.udid).toBe('device-iphone16pro'); expect(response.summary.simulator.name).toBe('iPhone 16 Pro'); }); it('should include bundle ID in summary', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.summary.bundleId).toBe('com.example.MyApp'); }); it('should provide next steps guidance', async () => { const result = await buildAndRunTool({ 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 suggest next interactions expect(response.guidance.some((g: string) => g.includes('simctl-tap'))).toBe(true); expect(response.guidance.some((g: string) => g.includes('simctl-type'))).toBe(true); }); it('should use specified configuration', async () => { const { xcodebuildBuildTool } = await import('../../../src/tools/xcodebuild/build.js'); await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, configuration: 'Release', }); expect(xcodebuildBuildTool).toHaveBeenCalledWith( expect.objectContaining({ configuration: 'Release', }) ); }); it('should default to Debug configuration', async () => { const { xcodebuildBuildTool } = await import('../../../src/tools/xcodebuild/build.js'); await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(xcodebuildBuildTool).toHaveBeenCalledWith( expect.objectContaining({ configuration: 'Debug', }) ); }); it('should use specified simulator UDID when provided', async () => { const { simctlSuggestTool } = await import('../../../src/tools/simctl/suggest.js'); const { simctlBootTool } = await import('../../../src/tools/simctl/boot.js'); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, simulatorUdid: 'custom-udid-123', }); // Should not call suggest when UDID is provided expect(simctlSuggestTool).not.toHaveBeenCalled(); // Should boot the specified simulator expect(simctlBootTool).toHaveBeenCalledWith( expect.objectContaining({ udid: 'custom-udid-123', }) ); const response = JSON.parse(result.content[0].text); expect(response.workflow.steps.find((s: any) => s.name === 'suggest')).toBeDefined(); }); it('should pass launch arguments to app', async () => { const { simctlLaunchTool } = await import('../../../src/tools/simctl/launch.js'); const launchArgs = ['--test-mode', '--debug']; await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, launchArguments: launchArgs, }); expect(simctlLaunchTool).toHaveBeenCalledWith( expect.objectContaining({ arguments: launchArgs, }) ); }); it('should pass environment variables to app', async () => { const { simctlLaunchTool } = await import('../../../src/tools/simctl/launch.js'); const envVars = { API_KEY: 'test-123', DEBUG: 'true' }; await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, environmentVariables: envVars, }); expect(simctlLaunchTool).toHaveBeenCalledWith( expect.objectContaining({ environment: envVars, }) ); }); it('should take screenshot when requested', async () => { const { simctlIoTool } = await import('../../../src/tools/simctl/io.js'); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, takeScreenshot: true, }); expect(simctlIoTool).toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); const screenshotStep = response.workflow.steps.find((s: any) => s.name === 'screenshot'); expect(screenshotStep).toBeDefined(); expect(screenshotStep.success).toBe(true); }); it('should skip screenshot when not requested', async () => { const { simctlIoTool } = await import('../../../src/tools/simctl/io.js'); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, takeScreenshot: false, }); expect(simctlIoTool).not.toHaveBeenCalled(); const response = JSON.parse(result.content[0].text); const screenshotStep = response.workflow.steps.find((s: any) => s.name === 'screenshot'); expect(screenshotStep).toBeUndefined(); }); }); describe('input validation', () => { it('should reject missing projectPath', async () => { await expect( buildAndRunTool({ scheme: validScheme, }) ).rejects.toThrow(McpError); }); it('should reject missing scheme', async () => { await expect( buildAndRunTool({ projectPath: validProjectPath, }) ).rejects.toThrow(McpError); }); it('should reject empty projectPath', async () => { await expect( buildAndRunTool({ projectPath: '', scheme: validScheme, }) ).rejects.toThrow(McpError); }); it('should reject empty scheme', async () => { await expect( buildAndRunTool({ projectPath: validProjectPath, scheme: '', }) ).rejects.toThrow(McpError); }); }); describe('error handling', () => { it('should handle build failures', async () => { const { xcodebuildBuildTool } = await import('../../../src/tools/xcodebuild/build.js'); (xcodebuildBuildTool as jest.Mock).mockResolvedValueOnce({ content: [ { type: 'text', text: JSON.stringify({ success: false, summary: { firstError: 'Compilation failed' }, }), }, ], }); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.isError).toBe(true); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); expect(response.workflow.errors.length).toBeGreaterThan(0); }); it('should continue when boot fails (may already be booted)', async () => { const { simctlBootTool } = await import('../../../src/tools/simctl/boot.js'); (simctlBootTool as jest.Mock).mockRejectedValueOnce(new Error('Already booted')); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); // Workflow should continue despite boot error const response = JSON.parse(result.content[0].text); expect(response.workflow.steps.length).toBeGreaterThan(2); // Should have more than just build + suggest }); it('should handle simulator suggestion failures', async () => { const { simctlSuggestTool } = await import('../../../src/tools/simctl/suggest.js'); (simctlSuggestTool as jest.Mock).mockResolvedValueOnce({ content: [{ type: 'text', text: JSON.stringify({ suggestions: [] }) }], }); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.isError).toBe(true); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); }); it('should handle install failures', async () => { const { simctlInstallTool } = await import('../../../src/tools/simctl/install.js'); (simctlInstallTool as jest.Mock).mockRejectedValueOnce(new Error('Install failed')); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.isError).toBe(true); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); }); it('should handle launch failures', async () => { const { simctlLaunchTool } = await import('../../../src/tools/simctl/launch.js'); (simctlLaunchTool as jest.Mock).mockRejectedValueOnce(new Error('Launch failed')); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(result.isError).toBe(true); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(false); }); it('should not fail workflow if screenshot fails', async () => { const { simctlIoTool } = await import('../../../src/tools/simctl/io.js'); (simctlIoTool as jest.Mock).mockRejectedValueOnce(new Error('Screenshot failed')); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, takeScreenshot: true, }); // Workflow should still succeed expect(result.isError).toBe(false); const response = JSON.parse(result.content[0].text); expect(response.success).toBe(true); }); it('should provide debugging guidance on failure', async () => { const { xcodebuildBuildTool } = await import('../../../src/tools/xcodebuild/build.js'); (xcodebuildBuildTool as jest.Mock).mockResolvedValueOnce({ content: [{ type: 'text', text: JSON.stringify({ success: false }) }], }); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.guidance).toBeDefined(); expect(response.guidance.some((g: string) => g.includes('debug'))).toBe(true); expect(response.guidance.some((g: string) => g.includes('xcodebuild-build'))).toBe(true); }); it('should show completed steps on failure', async () => { const { simctlInstallTool } = await import('../../../src/tools/simctl/install.js'); (simctlInstallTool as jest.Mock).mockRejectedValueOnce(new Error('Install failed')); const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response.workflow.steps.length).toBeGreaterThanOrEqual(3); // build, suggest, boot completed expect(response.guidance.some((g: string) => g.includes('Steps completed'))).toBe(true); }); }); describe('response format', () => { it('should return proper MCP response format', async () => { const result = await buildAndRunTool({ 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'); }); it('should return valid JSON in text content', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); expect(() => JSON.parse(result.content[0].text)).not.toThrow(); }); it('should include all required response fields', async () => { const result = await buildAndRunTool({ projectPath: validProjectPath, scheme: validScheme, }); const response = JSON.parse(result.content[0].text); expect(response).toHaveProperty('success'); expect(response).toHaveProperty('workflow'); expect(response).toHaveProperty('summary'); expect(response).toHaveProperty('guidance'); }); }); });

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