From b7ff38bd630dcc987e3f4454e8d4b1f6e8efa760 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Thu, 21 Aug 2025 16:34:27 +0000 Subject: [PATCH 1/5] fix: add platform parameter to clean tool with iOS default - Add optional platform parameter to clean tool schema with enum validation - Default to iOS platform instead of hardcoded macOS for iOS-only projects - Maintain backward compatibility - existing calls use iOS by default - Add comprehensive tests for platform parameter functionality - Update tool description to document new platform parameter Fixes #99 Co-authored-by: Cameron Cooke --- .../tools/utilities/__tests__/clean.test.ts | 55 +++++++++++++++++++ src/mcp/tools/utilities/clean.ts | 13 ++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/mcp/tools/utilities/__tests__/clean.test.ts b/src/mcp/tools/utilities/__tests__/clean.test.ts index 3812b855..97522539 100644 --- a/src/mcp/tools/utilities/__tests__/clean.test.ts +++ b/src/mcp/tools/utilities/__tests__/clean.test.ts @@ -48,4 +48,59 @@ describe('clean (unified) tool', () => { const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? ''); expect(text).toContain('Invalid parameters'); }); + + it('uses iOS platform by default', async () => { + const mock = createMockExecutor({ success: true, output: 'clean success' }); + const result = await cleanLogic({ projectPath: '/p.xcodeproj', scheme: 'App' } as any, mock); + expect(result.isError).not.toBe(true); + + // Check that the executor was called with iOS platform arguments + expect(mock).toHaveBeenCalled(); + const commandArgs = mock.mock.calls[0][0]; + expect(commandArgs).toContain('-destination'); + expect(commandArgs).toContain('platform=iOS'); + }); + + it('accepts custom platform parameter', async () => { + const mock = createMockExecutor({ success: true, output: 'clean success' }); + const result = await cleanLogic({ + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'macOS' + } as any, mock); + expect(result.isError).not.toBe(true); + + // Check that the executor was called with macOS platform arguments + expect(mock).toHaveBeenCalled(); + const commandArgs = mock.mock.calls[0][0]; + expect(commandArgs).toContain('-destination'); + expect(commandArgs).toContain('platform=macOS'); + }); + + it('accepts iOS Simulator platform parameter', async () => { + const mock = createMockExecutor({ success: true, output: 'clean success' }); + const result = await cleanLogic({ + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'iOS Simulator' + } as any, mock); + expect(result.isError).not.toBe(true); + + // Check that the executor was called with iOS Simulator platform arguments + expect(mock).toHaveBeenCalled(); + const commandArgs = mock.mock.calls[0][0]; + expect(commandArgs).toContain('-destination'); + expect(commandArgs).toContain('platform=iOS Simulator'); + }); + + it('handler validation: rejects invalid platform values', async () => { + const result = await (tool as any).handler({ + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'InvalidPlatform' + }); + expect(result.isError).toBe(true); + const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? ''); + expect(text).toContain('Invalid parameters'); + }); }); diff --git a/src/mcp/tools/utilities/clean.ts b/src/mcp/tools/utilities/clean.ts index 5e0fa9a5..ad74e249 100644 --- a/src/mcp/tools/utilities/clean.ts +++ b/src/mcp/tools/utilities/clean.ts @@ -32,6 +32,10 @@ const baseOptions = { .describe( 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', ), + platform: z + .enum(['macOS', 'iOS', 'iOS Simulator', 'watchOS', 'watchOS Simulator', 'tvOS', 'tvOS Simulator', 'visionOS', 'visionOS Simulator']) + .optional() + .describe('Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator'), }; const baseSchemaObject = z.object({ @@ -67,6 +71,11 @@ export async function cleanLogic( 'Invalid parameters:\nscheme: scheme is required when workspacePath is provided.', ); } + + // Use provided platform or default to iOS + const targetPlatform = params.platform ?? 'iOS'; + const platformEnum = XcodePlatform[targetPlatform as keyof typeof XcodePlatform]; + const hasProjectPath = typeof params.projectPath === 'string'; const typedParams: SharedBuildParams = { ...(hasProjectPath @@ -83,7 +92,7 @@ export async function cleanLogic( return executeXcodeBuildCommand( typedParams, { - platform: XcodePlatform.macOS, + platform: platformEnum, logPrefix: 'Clean', }, false, @@ -95,7 +104,7 @@ export async function cleanLogic( export default { name: 'clean', description: - "Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })", + "Cleans build products for either a project or a workspace using xcodebuild. Provide exactly one of projectPath or workspacePath. Platform defaults to iOS if not specified. Example: clean({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme', platform: 'iOS' })", schema: baseSchemaObject.shape, handler: createTypedTool( cleanSchema as z.ZodType, From 78d4201bc5355c97a42d22bc918ec4d131506774 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 21 Aug 2025 18:57:47 +0100 Subject: [PATCH 2/5] fix: properly type platform mapping and ensure simulator platforms are mapped to device equivalents for clean operations - Fix TypeScript error by properly typing cleanPlatformMap as Partial> - Simulator platforms (iOS Simulator, watchOS Simulator, etc.) are mapped to their device equivalents (iOS, watchOS, etc.) since build products are shared between device and simulator platforms - This allows clean operations to work correctly for all platform types including simulators - All tests passing and local testing confirms iOS, iOS Simulator, and macOS platforms work correctly --- .../tools/utilities/__tests__/clean.test.ts | 89 ++++++++++++------- src/mcp/tools/utilities/clean.ts | 57 ++++++++++-- 2 files changed, 106 insertions(+), 40 deletions(-) diff --git a/src/mcp/tools/utilities/__tests__/clean.test.ts b/src/mcp/tools/utilities/__tests__/clean.test.ts index 97522539..59191cd1 100644 --- a/src/mcp/tools/utilities/__tests__/clean.test.ts +++ b/src/mcp/tools/utilities/__tests__/clean.test.ts @@ -50,54 +50,75 @@ describe('clean (unified) tool', () => { }); it('uses iOS platform by default', async () => { - const mock = createMockExecutor({ success: true, output: 'clean success' }); - const result = await cleanLogic({ projectPath: '/p.xcodeproj', scheme: 'App' } as any, mock); + let capturedCommand: string[] = []; + const mockExecutor = async (command: string[]) => { + capturedCommand = command; + return { success: true, output: 'clean success' }; + }; + + const result = await cleanLogic( + { projectPath: '/p.xcodeproj', scheme: 'App' } as any, + mockExecutor, + ); expect(result.isError).not.toBe(true); - - // Check that the executor was called with iOS platform arguments - expect(mock).toHaveBeenCalled(); - const commandArgs = mock.mock.calls[0][0]; - expect(commandArgs).toContain('-destination'); - expect(commandArgs).toContain('platform=iOS'); + + // Check that the command contains iOS platform destination + const commandStr = capturedCommand.join(' '); + expect(commandStr).toContain('-destination'); + expect(commandStr).toContain('platform=iOS'); }); it('accepts custom platform parameter', async () => { - const mock = createMockExecutor({ success: true, output: 'clean success' }); - const result = await cleanLogic({ - projectPath: '/p.xcodeproj', - scheme: 'App', - platform: 'macOS' - } as any, mock); + let capturedCommand: string[] = []; + const mockExecutor = async (command: string[]) => { + capturedCommand = command; + return { success: true, output: 'clean success' }; + }; + + const result = await cleanLogic( + { + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'macOS', + } as any, + mockExecutor, + ); expect(result.isError).not.toBe(true); - - // Check that the executor was called with macOS platform arguments - expect(mock).toHaveBeenCalled(); - const commandArgs = mock.mock.calls[0][0]; - expect(commandArgs).toContain('-destination'); - expect(commandArgs).toContain('platform=macOS'); + + // Check that the command contains macOS platform destination + const commandStr = capturedCommand.join(' '); + expect(commandStr).toContain('-destination'); + expect(commandStr).toContain('platform=macOS'); }); - it('accepts iOS Simulator platform parameter', async () => { - const mock = createMockExecutor({ success: true, output: 'clean success' }); - const result = await cleanLogic({ - projectPath: '/p.xcodeproj', - scheme: 'App', - platform: 'iOS Simulator' - } as any, mock); + it('accepts iOS Simulator platform parameter (maps to iOS for clean)', async () => { + let capturedCommand: string[] = []; + const mockExecutor = async (command: string[]) => { + capturedCommand = command; + return { success: true, output: 'clean success' }; + }; + + const result = await cleanLogic( + { + projectPath: '/p.xcodeproj', + scheme: 'App', + platform: 'iOS Simulator', + } as any, + mockExecutor, + ); expect(result.isError).not.toBe(true); - - // Check that the executor was called with iOS Simulator platform arguments - expect(mock).toHaveBeenCalled(); - const commandArgs = mock.mock.calls[0][0]; - expect(commandArgs).toContain('-destination'); - expect(commandArgs).toContain('platform=iOS Simulator'); + + // For clean operations, iOS Simulator should be mapped to iOS platform + const commandStr = capturedCommand.join(' '); + expect(commandStr).toContain('-destination'); + expect(commandStr).toContain('platform=iOS'); }); it('handler validation: rejects invalid platform values', async () => { const result = await (tool as any).handler({ projectPath: '/p.xcodeproj', scheme: 'App', - platform: 'InvalidPlatform' + platform: 'InvalidPlatform', }); expect(result.isError).toBe(true); const text = String(result.content?.[1]?.text ?? result.content?.[0]?.text ?? ''); diff --git a/src/mcp/tools/utilities/clean.ts b/src/mcp/tools/utilities/clean.ts index ad74e249..f18db019 100644 --- a/src/mcp/tools/utilities/clean.ts +++ b/src/mcp/tools/utilities/clean.ts @@ -33,9 +33,21 @@ const baseOptions = { 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', ), platform: z - .enum(['macOS', 'iOS', 'iOS Simulator', 'watchOS', 'watchOS Simulator', 'tvOS', 'tvOS Simulator', 'visionOS', 'visionOS Simulator']) + .enum([ + 'macOS', + 'iOS', + 'iOS Simulator', + 'watchOS', + 'watchOS Simulator', + 'tvOS', + 'tvOS Simulator', + 'visionOS', + 'visionOS Simulator', + ]) .optional() - .describe('Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator'), + .describe( + 'Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator', + ), }; const baseSchemaObject = z.object({ @@ -71,11 +83,32 @@ export async function cleanLogic( 'Invalid parameters:\nscheme: scheme is required when workspacePath is provided.', ); } - + // Use provided platform or default to iOS const targetPlatform = params.platform ?? 'iOS'; - const platformEnum = XcodePlatform[targetPlatform as keyof typeof XcodePlatform]; - + + // Map human-friendly platform names to XcodePlatform enum values + // This is safer than direct key lookup and handles the space-containing simulator names + const platformMap = { + macOS: XcodePlatform.macOS, + iOS: XcodePlatform.iOS, + 'iOS Simulator': XcodePlatform.iOSSimulator, + watchOS: XcodePlatform.watchOS, + 'watchOS Simulator': XcodePlatform.watchOSSimulator, + tvOS: XcodePlatform.tvOS, + 'tvOS Simulator': XcodePlatform.tvOSSimulator, + visionOS: XcodePlatform.visionOS, + 'visionOS Simulator': XcodePlatform.visionOSSimulator, + }; + + const platformEnum = platformMap[targetPlatform]; + if (!platformEnum) { + return createErrorResponse( + 'Parameter validation failed', + `Invalid parameters:\nplatform: unsupported value "${targetPlatform}".`, + ); + } + const hasProjectPath = typeof params.projectPath === 'string'; const typedParams: SharedBuildParams = { ...(hasProjectPath @@ -89,10 +122,22 @@ export async function cleanLogic( extraArgs: params.extraArgs, }; + // For clean operations, simulator platforms should be mapped to their device equivalents + // since clean works at the build product level, not runtime level, and build products + // are shared between device and simulator platforms + const cleanPlatformMap: Partial> = { + [XcodePlatform.iOSSimulator]: XcodePlatform.iOS, + [XcodePlatform.watchOSSimulator]: XcodePlatform.watchOS, + [XcodePlatform.tvOSSimulator]: XcodePlatform.tvOS, + [XcodePlatform.visionOSSimulator]: XcodePlatform.visionOS, + }; + + const cleanPlatform = cleanPlatformMap[platformEnum] ?? platformEnum; + return executeXcodeBuildCommand( typedParams, { - platform: platformEnum, + platform: cleanPlatform, logPrefix: 'Clean', }, false, From 2d07e8a13a761c2bbe383bdec6cdf5388fddb291 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 21 Aug 2025 20:11:16 +0100 Subject: [PATCH 3/5] Update workflows --- .github/workflows/ci.yml | 14 ++++---- .github/workflows/pkg-pr-new.yml | 56 -------------------------------- 2 files changed, 8 insertions(+), 62 deletions(-) delete mode 100644 .github/workflows/pkg-pr-new.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d07743fa..b9400580 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,27 +16,29 @@ jobs: steps: - uses: actions/checkout@v3 - + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' - + - name: Install dependencies run: npm ci - + - name: Build run: npm run build - + - name: Lint run: npm run lint - + - name: Check formatting run: npm run format:check - + - name: Type check run: npm run typecheck - name: Run tests run: npm test + + - run: pnpm dlx pkg-pr-new publish diff --git a/.github/workflows/pkg-pr-new.yml b/.github/workflows/pkg-pr-new.yml deleted file mode 100644 index 2afbf690..00000000 --- a/.github/workflows/pkg-pr-new.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: Publish Package Previews - -on: - pull_request_review: - types: [submitted] - pull_request: - types: [opened, synchronize] - -permissions: - contents: read - pull-requests: write - -jobs: - # Publish on all PRs (for testing/development) - publish-pr: - if: github.event_name == 'pull_request' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Publish to pkg.pr.new - run: npx pkg-pr-new publish --comment=update - - # Publish on approved PRs (recommended pattern) - publish-approved: - if: github.event_name == 'pull_request_review' && github.event.review.state == 'approved' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Build - run: npm run build - - - name: Publish to pkg.pr.new (Approved) - run: npx pkg-pr-new publish --comment=create \ No newline at end of file From 917cb8a7f581ff09e65b97e765011cc639b99318 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 21 Aug 2025 20:12:51 +0100 Subject: [PATCH 4/5] Fix workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b9400580..98b195d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,4 +41,4 @@ jobs: - name: Run tests run: npm test - - run: pnpm dlx pkg-pr-new publish + - run: npm dlx pkg-pr-new publish From c06c714e024358fad44798a7f66277a2cc730983 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 21 Aug 2025 20:14:56 +0100 Subject: [PATCH 5/5] Fix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98b195d4..17a06a74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,4 +41,4 @@ jobs: - name: Run tests run: npm test - - run: npm dlx pkg-pr-new publish + - run: npx pkg-pr-new publish