Skip to main content
Glama

MCP Xcode

by Stefan-Nitu
hook-e2e.test.skip18 kB
/** * End-to-end tests for the xcode-sync hook */ import { describe, test, expect, beforeAll, afterAll, afterEach } from '@jest/globals'; import { execSync } from 'child_process'; import { existsSync, readFileSync, writeFileSync, rmSync, copyFileSync, mkdirSync } from 'fs'; import { join } from 'path'; describe('Xcode Sync Hook E2E', () => { const hookScript = join(process.cwd(), 'scripts', 'xcode-sync.swift'); const testArtifactsDir = join(process.cwd(), 'test_artifacts'); // Test projects const xcodeProjectPath = join(testArtifactsDir, 'TestProjectSwiftTesting'); const xcodePbxproj = join(xcodeProjectPath, 'TestProjectSwiftTesting.xcodeproj', 'project.pbxproj'); const xcodePbxprojBackup = `${xcodePbxproj}.backup`; const spmProjectPath = join(testArtifactsDir, 'TestSwiftPackageXCTest'); beforeAll(() => { // Build XcodeProjectModifier if needed if (!existsSync('XcodeProjectModifier/.build/release/XcodeProjectModifier')) { execSync('npm run build:modifier', { stdio: 'inherit' }); } // Backup Xcode project file if (existsSync(xcodePbxproj)) { copyFileSync(xcodePbxproj, xcodePbxprojBackup); } }); afterAll(() => { // Restore Xcode project file if (existsSync(xcodePbxprojBackup)) { copyFileSync(xcodePbxprojBackup, xcodePbxproj); rmSync(xcodePbxprojBackup); } }); afterEach(() => { // Restore project file after each test if (existsSync(xcodePbxprojBackup)) { copyFileSync(xcodePbxprojBackup, xcodePbxproj); } // Clean up test files const testFiles = [ join(xcodeProjectPath, 'TestProjectSwiftTesting', 'TestFile.swift'), join(xcodeProjectPath, 'TestProjectSwiftTesting', 'ResourceFile.json'), join(xcodeProjectPath, 'TestProjectSwiftTesting', 'TouchedFile.swift'), join(xcodeProjectPath, 'TestProjectSwiftTesting', 'EchoFile.swift'), join(xcodeProjectPath, 'TestProjectSwiftTesting', 'FirstAdd.swift'), join(xcodeProjectPath, 'TestProjectSwiftTesting', 'File With Spaces.swift'), join(xcodeProjectPath, 'TestProjectSwiftTesting', 'test.xyz'), join(spmProjectPath, 'Sources', 'TestSwiftPackageXCTest', 'TestFile.swift'), ]; testFiles.forEach(file => { if (existsSync(file)) { rmSync(file); } }); }); describe('Xcode Project Files', () => { test('should add Swift file to Xcode project', () => { const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'TestFile.swift'); const testContent = 'import Foundation\n\nstruct TestFile {}'; // Create the file writeFileSync(testFile, testContent); // Create hook data const hookData = { tool_name: 'Write', tool_input: { file_path: testFile, content: testContent }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Check output expect(result).toContain('Added TestFile.swift to project'); // Verify the project file was modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).toContain('TestFile.swift'); // Count occurrences (should be multiple for different sections) const matches = pbxprojContent.match(/TestFile\.swift/g); expect(matches).toBeTruthy(); expect(matches!.length).toBeGreaterThan(1); }); test('should add resource file to Xcode project', () => { const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'ResourceFile.json'); const testContent = '{"key": "value"}'; // Create the file writeFileSync(testFile, testContent); // Create hook data const hookData = { tool_name: 'Write', tool_input: { file_path: testFile, content: testContent }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Check output expect(result).toContain('Added ResourceFile.json to project'); // Verify the project file was modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).toContain('ResourceFile.json'); }); test('should not add file when updating existing file', () => { const existingFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'ContentView.swift'); const originalContent = readFileSync(xcodePbxproj, 'utf-8'); // Create hook data for edit operation const hookData = { tool_name: 'Edit', tool_input: { file_path: existingFile, old_string: 'struct ContentView', new_string: 'struct ContentView // Modified' }, tool_response: { type: 'update' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Should not detect any operation for updates expect(result).not.toContain('Added'); expect(result).not.toContain('Removed'); // Project file should remain unchanged const newContent = readFileSync(xcodePbxproj, 'utf-8'); expect(newContent).toBe(originalContent); }); }); describe('Swift Package Manager', () => { test('should NOT add file to Xcode project when file is in SPM', () => { // First check if SPM project has Package.swift const packageSwiftPath = join(spmProjectPath, 'Package.swift'); expect(existsSync(packageSwiftPath)).toBe(true); const testFile = join(spmProjectPath, 'Sources', 'TestSPM', 'TestFile.swift'); const testContent = 'import Foundation\n\nstruct TestFile {}'; // Create directories if needed const sourceDir = join(spmProjectPath, 'Sources', 'TestSwiftPackageXCTest'); if (!existsSync(sourceDir)) { mkdirSync(sourceDir, { recursive: true }); } // Create the file writeFileSync(testFile, testContent); // Create hook data const hookData = { tool_name: 'Write', tool_input: { file_path: testFile, content: testContent }, tool_response: { type: 'create' }, cwd: spmProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Should detect SPM and skip expect(result).toContain('File is part of a Swift Package - skipping Xcode project sync'); // If there's an .xcodeproj in SPM (shouldn't be), it shouldn't be modified const spmXcodeProj = join(spmProjectPath, 'TestSwiftPackageXCTest.xcodeproj', 'project.pbxproj'); if (existsSync(spmXcodeProj)) { const content = readFileSync(spmXcodeProj, 'utf-8'); expect(content).not.toContain('TestFile.swift'); } }); test('should NOT add SPM files to Xcode project that contains a local Swift Package', () => { // Create a local Swift Package inside the Xcode project const localPackageDir = join(xcodeProjectPath, 'LocalPackage'); const packageSwiftPath = join(localPackageDir, 'Package.swift'); const sourcesDir = join(localPackageDir, 'Sources', 'LocalPackage'); // Create Package.swift mkdirSync(localPackageDir, { recursive: true }); writeFileSync(packageSwiftPath, `// swift-tools-version: 5.9 import PackageDescription let package = Package( name: "LocalPackage", products: [ .library(name: "LocalPackage", targets: ["LocalPackage"]), ], targets: [ .target(name: "LocalPackage"), ] )`); // Create a Swift file in the local package mkdirSync(sourcesDir, { recursive: true }); const spmFile = join(sourcesDir, 'LocalFeature.swift'); writeFileSync(spmFile, 'public struct LocalFeature {}'); // Create hook data for adding file to the local SPM const hookData = { tool_name: 'Write', tool_input: { file_path: spmFile, content: 'public struct LocalFeature {}' }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Should detect that the file is in a Swift Package expect(result).toContain('File is part of a Swift Package - skipping Xcode project sync'); // Verify the Xcode project file was NOT modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).not.toContain('LocalFeature.swift'); expect(pbxprojContent).not.toContain('LocalPackage'); // Clean up rmSync(localPackageDir, { recursive: true, force: true }); }); test('should add regular files to Xcode project even when it contains local SPM', () => { // Create a local Swift Package inside the Xcode project const localPackageDir = join(xcodeProjectPath, 'LocalPackage'); const packageSwiftPath = join(localPackageDir, 'Package.swift'); mkdirSync(localPackageDir, { recursive: true }); writeFileSync(packageSwiftPath, `// swift-tools-version: 5.9 import PackageDescription let package = Package(name: "LocalPackage")`); // Now add a regular file (NOT in the SPM directory) const regularFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'RegularFile.swift'); writeFileSync(regularFile, 'struct RegularFile {}'); // Create hook data for the regular file const hookData = { tool_name: 'Write', tool_input: { file_path: regularFile, content: 'struct RegularFile {}' }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Should add the regular file normally expect(result).toContain('Added RegularFile.swift to project'); // Verify the Xcode project file WAS modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).toContain('RegularFile.swift'); // Clean up rmSync(localPackageDir, { recursive: true, force: true }); rmSync(regularFile); }); }); describe('Bash Commands', () => { test('should add file created with touch command', () => { const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'TouchedFile.swift'); // Create hook data for touch command const hookData = { tool_name: 'Bash', tool_input: { command: `touch "${testFile}"` }, tool_response: { stdout: '', stderr: '' }, cwd: xcodeProjectPath }; // Create the file (simulating touch) writeFileSync(testFile, ''); // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Check output expect(result).toContain('Detected add operation'); expect(result).toContain('TouchedFile.swift'); // Verify the project file was modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).toContain('TouchedFile.swift'); // Clean up rmSync(testFile); }); test('should ignore non-file operations', () => { const originalContent = readFileSync(xcodePbxproj, 'utf-8'); const commands = [ 'echo "Hello World"', // No redirection 'ls -la', 'pwd', 'git status', 'npm install', 'swift build', 'ps aux', 'date', 'whoami' ]; commands.forEach(command => { const hookData = { tool_name: 'Bash', tool_input: { command }, tool_response: { stdout: 'some output' }, cwd: xcodeProjectPath }; const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Should not detect any file operations expect(result).not.toContain('Detected add operation'); expect(result).not.toContain('Detected remove operation'); }); // Project file should remain unchanged const newContent = readFileSync(xcodePbxproj, 'utf-8'); expect(newContent).toBe(originalContent); }); test('should detect file creation with echo redirection', () => { const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'EchoFile.swift'); // Create hook data for echo with redirection const hookData = { tool_name: 'Bash', tool_input: { command: `echo "struct EchoFile {}" > "${testFile}"` }, tool_response: { stdout: '', stderr: '' }, cwd: xcodeProjectPath }; // Create the file (simulating echo >) writeFileSync(testFile, 'struct EchoFile {}'); // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Check output expect(result).toContain('Detected add operation'); expect(result).toContain('EchoFile.swift'); // Verify the project file was modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).toContain('EchoFile.swift'); // Clean up rmSync(testFile); }); }); describe('Duplicate Detection', () => { test('should NOT add file that is already in project', () => { // First add a file const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'FirstAdd.swift'); writeFileSync(testFile, 'struct FirstAdd {}'); const hookData1 = { tool_name: 'Write', tool_input: { file_path: testFile, content: 'struct FirstAdd {}' }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Add it once execSync(hookScript, { input: JSON.stringify(hookData1), encoding: 'utf-8' }); // Verify it was added let pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); const firstCount = (pbxprojContent.match(/FirstAdd\.swift/g) || []).length; expect(firstCount).toBeGreaterThan(0); // Try to add it again const result = execSync(hookScript, { input: JSON.stringify(hookData1), encoding: 'utf-8' }); // Should skip duplicate expect(result).toContain('File is already in the project - skipping'); // Count should remain the same pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); const secondCount = (pbxprojContent.match(/FirstAdd\.swift/g) || []).length; expect(secondCount).toBe(firstCount); // Clean up rmSync(testFile); }); }); describe('Edge Cases', () => { test('should handle files with spaces in names', () => { const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'File With Spaces.swift'); const testContent = 'import Foundation\n\nstruct FileWithSpaces {}'; // Create the file writeFileSync(testFile, testContent); // Create hook data const hookData = { tool_name: 'Write', tool_input: { file_path: testFile, content: testContent }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Check output expect(result).toContain('Added File With Spaces.swift to project'); // Verify the project file was modified const pbxprojContent = readFileSync(xcodePbxproj, 'utf-8'); expect(pbxprojContent).toContain('File With Spaces.swift'); // Clean up rmSync(testFile); }); test('should skip unsupported file types', () => { const testFile = join(xcodeProjectPath, 'TestProjectSwiftTesting', 'test.xyz'); const originalContent = readFileSync(xcodePbxproj, 'utf-8'); // Create the file writeFileSync(testFile, 'unsupported content'); // Create hook data const hookData = { tool_name: 'Write', tool_input: { file_path: testFile, content: 'content' }, tool_response: { type: 'create' }, cwd: xcodeProjectPath }; // Run the hook const result = execSync(hookScript, { input: JSON.stringify(hookData), encoding: 'utf-8' }); // Should not add unsupported file types expect(result).not.toContain('Added test.xyz'); // Project file should remain unchanged const newContent = readFileSync(xcodePbxproj, 'utf-8'); expect(newContent).toBe(originalContent); // Clean up rmSync(testFile); }); }); });

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/Stefan-Nitu/mcp-xcode'

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