diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ea9e34..6d6c288c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Added configurable file artifact text rendering with CLI output defaulting to labeled `Files:` lists, MCP text preserving compact trees, and `filePathRenderStyle` / `XCODEBUILDMCP_FILE_PATH_RENDER_STYLE` / `--file-path-render-style` overrides. - Added workspace-scoped default xcresult bundles for simulator, device, and macOS test tools so test artifacts are available in structured and text output even when callers do not pass `-resultBundlePath`. - Added opt-in MCP server idle shutdown via `XCODEBUILDMCP_MCP_IDLE_TIMEOUT_MS`, allowing unused MCP server processes to gracefully exit after a configured idle period ([#394](https://github.com/getsentry/XcodeBuildMCP/issues/394)). +- Added canonical `launchArgs` launch-argument input across build-and-run tools (`build_run_sim`, `build_run_device`, `build_run_macos`) and launch-only tools (`launch_app_sim`, `launch_app_device`, `launch_mac_app`) so runtime launch arguments are passed only to app launch steps; `extraArgs` remains build-system-only and launch-only tools no longer use generic `args`. ### Fixed diff --git a/src/mcp/tools/device/__tests__/build_run_device.test.ts b/src/mcp/tools/device/__tests__/build_run_device.test.ts index 5463e349..20764a79 100644 --- a/src/mcp/tools/device/__tests__/build_run_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_run_device.test.ts @@ -32,6 +32,7 @@ describe('build_run_device tool', () => { expect(schemaObj.safeParse({}).success).toBe(true); expect(schemaObj.safeParse({ extraArgs: ['-quiet'] }).success).toBe(true); + expect(schemaObj.safeParse({ launchArgs: ['--uitesting'] }).success).toBe(true); expect(schemaObj.safeParse({ env: { FOO: 'bar' } }).success).toBe(true); expect(schemaObj.safeParse({ platform: 'tvOS' }).success).toBe(true); expect(schemaObj.safeParse({ platform: 'tvOS Simulator' }).success).toBe(true); @@ -40,8 +41,10 @@ describe('build_run_device tool', () => { expect(schemaObj.safeParse({ scheme: 'App' }).success).toBe(false); expect(schemaObj.safeParse({ deviceId: 'device-id' }).success).toBe(false); + expect(schemaObj.safeParse({ launchArgs: [123] }).success).toBe(false); + const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['env', 'extraArgs', 'platform']); + expect(schemaKeys).toEqual(['env', 'extraArgs', 'launchArgs', 'platform']); }); }); @@ -246,6 +249,68 @@ describe('build_run_device tool', () => { expect(text).not.toContain('Process ID'); }); + it('passes launchArgs only to launch command and keeps extraArgs on xcodebuild commands', async () => { + const commandCalls: string[][] = []; + const mockExecutor: CommandExecutor = async (command) => { + commandCalls.push(command); + + if (command.includes('-showBuildSettings')) { + return createMockCommandResponse({ + success: true, + output: 'BUILT_PRODUCTS_DIR = /tmp/build\nFULL_PRODUCT_NAME = MyWatchApp.app\n', + }); + } + + if (command[0] === 'defaults' || command[0] === '/usr/libexec/PlistBuddy') { + return createMockCommandResponse({ success: true, output: 'io.sentry.MyWatchApp' }); + } + + if (command.includes('launch')) { + return createMockCommandResponse({ + success: true, + output: JSON.stringify({ result: { process: { processIdentifier: 9876 } } }), + }); + } + + return createMockCommandResponse({ success: true, output: 'OK' }); + }; + + const { result } = await runBuildRunDeviceLogic( + { + projectPath: '/tmp/MyWatchApp.xcodeproj', + scheme: 'MyWatchApp', + platform: 'watchOS', + deviceId: 'DEVICE-UDID', + extraArgs: ['-quiet'], + launchArgs: ['--uitesting', '--reset-state'], + }, + mockExecutor, + createMockFileSystemExecutor({ existsSync: () => true }), + ); + + expectPendingBuildRunResponse(result, false); + + const xcodebuildCommands = commandCalls.filter((command) => command[0] === 'xcodebuild'); + expect(xcodebuildCommands.length).toBeGreaterThan(0); + for (const command of xcodebuildCommands) { + expect(command).toContain('-quiet'); + expect(command).not.toContain('--uitesting'); + expect(command).not.toContain('--reset-state'); + } + + const launchCommand = commandCalls.find( + (command) => + command[0] === 'xcrun' && + command[1] === 'devicectl' && + command[2] === 'device' && + command[3] === 'process' && + command[4] === 'launch', + ); + expect(launchCommand).toBeDefined(); + expect(launchCommand).toContain('--uitesting'); + expect(launchCommand).toContain('--reset-state'); + }); + it('uses generic destination for build-settings lookup', async () => { const commandCalls: string[][] = []; const mockExecutor: CommandExecutor = async (command) => { diff --git a/src/mcp/tools/device/__tests__/launch_app_device.test.ts b/src/mcp/tools/device/__tests__/launch_app_device.test.ts index e1c18163..9b0d1829 100644 --- a/src/mcp/tools/device/__tests__/launch_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/launch_app_device.test.ts @@ -22,7 +22,9 @@ describe('launch_app_device plugin (device-shared)', () => { const schemaObj = z.strictObject(schema); expect(schemaObj.safeParse({}).success).toBe(true); expect(schemaObj.safeParse({ bundleId: 'io.sentry.app' }).success).toBe(false); - expect(Object.keys(schema).sort()).toEqual(['env']); + expect(schemaObj.safeParse({ launchArgs: ['--uitesting'] }).success).toBe(true); + expect(schemaObj.safeParse({ args: ['--legacy'] }).success).toBe(false); + expect(Object.keys(schema).sort()).toEqual(['env', 'launchArgs']); }); it('should validate schema with invalid inputs', () => { @@ -124,6 +126,37 @@ describe('launch_app_device plugin (device-shared)', () => { expect(JSON.parse(cmd[envIdx + 1])).toEqual({ STAGING_ENABLED: '1', DEBUG: 'true' }); }); + it('should append launchArgs after bundleId when launchArgs is provided', async () => { + const calls: any[] = []; + const mockExecutor = createMockExecutor({ + success: true, + output: 'App launched successfully', + process: { pid: 12345 }, + }); + + const trackingExecutor = async (command: string[]) => { + calls.push({ command }); + return mockExecutor(command); + }; + + await runLogic(() => + launch_app_deviceLogic( + { + deviceId: 'test-device-123', + bundleId: 'io.sentry.app', + launchArgs: ['--uitesting', '--reset-state'], + }, + trackingExecutor, + createMockFileSystemExecutor(), + ), + ); + + const cmd = calls[0].command; + const bundleIdIndex = cmd.indexOf('io.sentry.app'); + expect(bundleIdIndex).toBeGreaterThan(-1); + expect(cmd.slice(bundleIdIndex + 1)).toEqual(['--uitesting', '--reset-state']); + }); + it('should not include --environment-variables when env is not provided', async () => { const calls: any[] = []; const mockExecutor = createMockExecutor({ diff --git a/src/mcp/tools/device/build_run_device.ts b/src/mcp/tools/device/build_run_device.ts index ae590177..ca2b44b0 100644 --- a/src/mcp/tools/device/build_run_device.ts +++ b/src/mcp/tools/device/build_run_device.ts @@ -58,7 +58,14 @@ const baseSchemaObject = z.object({ platform: devicePlatformSchema, configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), derivedDataPath: z.string().optional(), - extraArgs: z.array(z.string()).optional(), + extraArgs: z + .array(z.string()) + .optional() + .describe('Additional xcodebuild/build-settings arguments (not app launch arguments)'), + launchArgs: z + .array(z.string()) + .optional() + .describe('Arguments passed to the launched app process on physical device runtime'), preferXcodebuild: z.boolean().optional(), env: z .record(z.string(), z.string()) @@ -229,7 +236,7 @@ export function createBuildRunDeviceExecutor( bundleId, executor, fileSystemExecutor, - { env: params.env }, + { env: params.env, args: params.launchArgs }, ); if (!launchResult.success) { const errorMessage = launchResult.error ?? 'Failed to launch app'; diff --git a/src/mcp/tools/device/launch_app_device.ts b/src/mcp/tools/device/launch_app_device.ts index 4a2665bc..72c18b22 100644 --- a/src/mcp/tools/device/launch_app_device.ts +++ b/src/mcp/tools/device/launch_app_device.ts @@ -31,6 +31,10 @@ import { const launchAppDeviceSchema = z.object({ deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), bundleId: z.string(), + launchArgs: z + .array(z.string()) + .optional() + .describe('Arguments passed to the launched app process on physical device runtime'), env: z .record(z.string(), z.string()) .optional() @@ -88,6 +92,7 @@ export function createLaunchAppDeviceExecutor( fileSystem, { env: params.env, + args: params.launchArgs, }, ); diff --git a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts index 27d8201b..b8669446 100644 --- a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts @@ -31,13 +31,15 @@ describe('build_run_macos', () => { expect(zodSchema.safeParse({}).success).toBe(true); expect(zodSchema.safeParse({ extraArgs: ['--verbose'] }).success).toBe(true); + expect(zodSchema.safeParse({ launchArgs: ['--uitesting'] }).success).toBe(true); expect(zodSchema.safeParse({ derivedDataPath: '/tmp/derived' }).success).toBe(false); expect(zodSchema.safeParse({ extraArgs: ['--ok', 2] }).success).toBe(false); + expect(zodSchema.safeParse({ launchArgs: ['--ok', 2] }).success).toBe(false); expect(zodSchema.safeParse({ preferXcodebuild: true }).success).toBe(false); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs']); + expect(schemaKeys).toEqual(['extraArgs', 'launchArgs']); }); }); @@ -398,6 +400,71 @@ describe('build_run_macos', () => { expect(result.nextStepParams).toBeUndefined(); }); + it('should pass launchArgs only to app launch and keep extraArgs on xcodebuild commands', async () => { + let callCount = 0; + const executorCalls: any[] = []; + const mockExecutor = ( + command: string[], + description?: string, + logOutput?: boolean, + opts?: { cwd?: string }, + detached?: boolean, + ) => { + callCount++; + executorCalls.push({ command, description, logOutput, opts }); + void detached; + + if (callCount === 1) { + return Promise.resolve({ + success: true, + output: 'BUILD SUCCEEDED', + error: '', + process: mockProcess, + }); + } else if (callCount === 2) { + return Promise.resolve({ + success: true, + output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app', + error: '', + process: mockProcess, + }); + } + return Promise.resolve({ success: true, output: '', error: '', process: mockProcess }); + }; + + const args = { + projectPath: '/path/to/project.xcodeproj', + scheme: 'MyApp', + configuration: 'Debug', + preferXcodebuild: false, + extraArgs: ['-quiet'], + launchArgs: ['--uitesting', '--reset-state'], + }; + + await runBuildRunMacOSLogic(args, mockExecutor); + + const xcodebuildCommands = executorCalls + .map(({ command }) => command) + .filter((command) => command[0] === 'xcodebuild'); + expect(xcodebuildCommands.length).toBeGreaterThan(0); + for (const command of xcodebuildCommands) { + expect(command).toContain('-quiet'); + expect(command).not.toContain('--uitesting'); + expect(command).not.toContain('--reset-state'); + } + + const openCommand = executorCalls + .map(({ command }) => command) + .find((command) => command[0] === 'open'); + expect(openCommand).toEqual([ + 'open', + '/path/to/build/MyApp.app', + '--args', + '--uitesting', + '--reset-state', + ]); + }); + it('should use default configuration when not provided', async () => { let callCount = 0; const executorCalls: any[] = []; diff --git a/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts b/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts index f291f552..66d88ecd 100644 --- a/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts @@ -23,15 +23,22 @@ describe('launch_mac_app plugin', () => { expect( zodSchema.safeParse({ appPath: '/Applications/Calculator.app', - args: ['--debug'], + launchArgs: ['--debug'], }).success, ).toBe(true); expect( zodSchema.safeParse({ appPath: '/path/to/MyApp.app', - args: ['--debug', '--verbose'], + launchArgs: ['--debug', '--verbose'], }).success, ).toBe(true); + const strictSchema = z.strictObject(schema); + expect( + strictSchema.safeParse({ + appPath: '/path/to/MyApp.app', + args: ['--legacy'], + }).success, + ).toBe(false); }); it('should validate schema with invalid inputs', () => { @@ -40,7 +47,7 @@ describe('launch_mac_app plugin', () => { expect(zodSchema.safeParse({ appPath: null }).success).toBe(false); expect(zodSchema.safeParse({ appPath: 123 }).success).toBe(false); expect( - zodSchema.safeParse({ appPath: '/path/to/MyApp.app', args: 'not-array' }).success, + zodSchema.safeParse({ appPath: '/path/to/MyApp.app', launchArgs: 'not-array' }).success, ).toBe(false); }); }); @@ -93,7 +100,7 @@ describe('launch_mac_app plugin', () => { expect(calls[0].command).toEqual(['open', '/path/to/MyApp.app']); }); - it('should generate correct command with args parameter', async () => { + it('should generate correct command with launchArgs parameter', async () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); @@ -108,7 +115,7 @@ describe('launch_mac_app plugin', () => { launch_mac_appLogic( { appPath: '/path/to/MyApp.app', - args: ['--debug', '--verbose'], + launchArgs: ['--debug', '--verbose'], }, mockExecutor, mockFileSystem, @@ -124,7 +131,7 @@ describe('launch_mac_app plugin', () => { ]); }); - it('should generate correct command with empty args array', async () => { + it('should generate correct command with empty launchArgs array', async () => { const calls: any[] = []; const mockExecutor = async (command: string[]) => { calls.push({ command }); @@ -139,7 +146,7 @@ describe('launch_mac_app plugin', () => { launch_mac_appLogic( { appPath: '/path/to/MyApp.app', - args: [], + launchArgs: [], }, mockExecutor, mockFileSystem, diff --git a/src/mcp/tools/macos/build_run_macos.ts b/src/mcp/tools/macos/build_run_macos.ts index a11efb12..9f3e8f71 100644 --- a/src/mcp/tools/macos/build_run_macos.ts +++ b/src/mcp/tools/macos/build_run_macos.ts @@ -49,7 +49,14 @@ const baseSchemaObject = z.object({ .enum(['arm64', 'x86_64']) .optional() .describe('Architecture to build for (arm64 or x86_64). For macOS only.'), - extraArgs: z.array(z.string()).optional(), + extraArgs: z + .array(z.string()) + .optional() + .describe('Additional xcodebuild/build-settings arguments (not app launch arguments)'), + launchArgs: z + .array(z.string()) + .optional() + .describe('Arguments passed to the launched app process on macOS runtime'), preferXcodebuild: z.boolean().optional(), }); @@ -153,7 +160,7 @@ export function createBuildRunMacOSExecutor( status: 'started', }); - const macLaunchResult = await launchMacApp(appPath, executor); + const macLaunchResult = await launchMacApp(appPath, executor, { args: params.launchArgs }); if (!macLaunchResult.success) { return createBuildRunDomainResult({ started, diff --git a/src/mcp/tools/macos/launch_mac_app.ts b/src/mcp/tools/macos/launch_mac_app.ts index 9357ad14..d4b5cedc 100644 --- a/src/mcp/tools/macos/launch_mac_app.ts +++ b/src/mcp/tools/macos/launch_mac_app.ts @@ -16,7 +16,10 @@ import { const launchMacAppSchema = z.object({ appPath: z.string(), - args: z.array(z.string()).optional(), + launchArgs: z + .array(z.string()) + .optional() + .describe('Arguments passed to the launched app process on macOS runtime'), }); type LaunchMacAppParams = z.infer; @@ -57,7 +60,7 @@ export function createLaunchMacAppExecutor( log('info', `Starting launch macOS app request for ${params.appPath}`); try { - const result = await launchMacApp(params.appPath, executor, { args: params.args }); + const result = await launchMacApp(params.appPath, executor, { args: params.launchArgs }); if (!result.success) { return buildLaunchFailure( diff --git a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts index 25c7bb49..6c211731 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -54,13 +54,19 @@ describe('build_run_sim tool', () => { extraArgs: ['--verbose'], }).success, ).toBe(true); + expect( + schemaObj.safeParse({ + launchArgs: ['--uitesting'], + }).success, + ).toBe(true); expect(schemaObj.safeParse({ derivedDataPath: '/path/to/derived' }).success).toBe(false); expect(schemaObj.safeParse({ extraArgs: [123] }).success).toBe(false); + expect(schemaObj.safeParse({ launchArgs: [123] }).success).toBe(false); expect(schemaObj.safeParse({ preferXcodebuild: false }).success).toBe(false); const schemaKeys = Object.keys(schema).sort(); - expect(schemaKeys).toEqual(['extraArgs']); + expect(schemaKeys).toEqual(['extraArgs', 'launchArgs']); expect(schemaKeys).not.toContain('scheme'); expect(schemaKeys).not.toContain('simulatorName'); expect(schemaKeys).not.toContain('projectPath'); @@ -470,6 +476,89 @@ describe('build_run_sim tool', () => { expect(callHistory[1].logPrefix).toBe('iOS Simulator Build'); }); + it('should pass launchArgs only to launcher and keep extraArgs on xcodebuild commands', async () => { + const callHistory: Array<{ command: string[]; logPrefix?: string }> = []; + let launchArgs: string[] | undefined; + + const trackingLauncher: SimulatorLauncher = async (_uuid, _bundleId, _executor, opts) => { + launchArgs = opts?.args; + return { + success: true, + processId: 11111, + logFilePath: '/tmp/mock-logs/test.log', + }; + }; + + const trackingExecutor: CommandExecutor = async (command, logPrefix) => { + callHistory.push({ command, logPrefix }); + + if (command[0] === 'xcrun' && command[1] === 'simctl' && command[2] === 'list') { + return createMockCommandResponse({ + success: true, + output: JSON.stringify({ + devices: { + 'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [ + { udid: 'test-uuid-123', name: 'iPhone 17', isAvailable: true, state: 'Booted' }, + ], + }, + }), + }); + } + if (command[0] === 'xcodebuild' && command.includes('build')) { + return createMockCommandResponse({ + success: true, + output: 'BUILD SUCCEEDED', + }); + } + if (command[0] === 'xcodebuild' && command.includes('-showBuildSettings')) { + return createMockCommandResponse({ + success: true, + output: 'BUILT_PRODUCTS_DIR = /path/to/build\nFULL_PRODUCT_NAME = MyApp.app\n', + }); + } + if ( + command.some( + (c) => c.includes('plutil') || c.includes('PlistBuddy') || c.includes('defaults'), + ) + ) { + return createMockCommandResponse({ + success: true, + output: 'io.sentry.MyApp', + }); + } + + return createMockCommandResponse({ + success: true, + output: 'Success', + }); + }; + + const { result } = await runBuildRunSimLogic( + { + workspacePath: '/path/to/MyProject.xcworkspace', + scheme: 'MyScheme', + simulatorName: 'iPhone 17', + extraArgs: ['-quiet'], + launchArgs: ['--uitesting', '--reset-state'], + }, + trackingExecutor, + trackingLauncher, + ); + + expectPendingBuildRunResponse(result, false); + expect(launchArgs).toEqual(['--uitesting', '--reset-state']); + + const xcodebuildCommands = callHistory + .map(({ command }) => command) + .filter((command) => command[0] === 'xcodebuild'); + expect(xcodebuildCommands.length).toBeGreaterThan(0); + for (const command of xcodebuildCommands) { + expect(command).toContain('-quiet'); + expect(command).not.toContain('--uitesting'); + expect(command).not.toContain('--reset-state'); + } + }); + it('should generate correct build settings command after successful build', async () => { const callHistory: Array<{ command: string[]; logPrefix?: string }> = []; diff --git a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts index e0124770..583c5ca9 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts @@ -28,14 +28,15 @@ describe('launch_app_sim tool', () => { expect( schemaObj.safeParse({ - args: ['--debug'], + launchArgs: ['--debug'], }).success, ).toBe(true); expect(schemaObj.safeParse({ bundleId: 'io.sentry.testapp' }).success).toBe(false); expect(schemaObj.safeParse({ bundleId: 123 }).success).toBe(false); - expect(Object.keys(schema).sort()).toEqual(['args', 'env']); + expect(schemaObj.safeParse({ args: ['--legacy'] }).success).toBe(false); + expect(Object.keys(schema).sort()).toEqual(['env', 'launchArgs']); const withSimDefaults = schemaObj.safeParse({ simulatorId: 'sim-default', @@ -109,7 +110,7 @@ describe('launch_app_sim tool', () => { }); }); - it('should pass args and env through to launcher', async () => { + it('should pass launchArgs and env through to launcher', async () => { let capturedArgs: string[] | undefined; let capturedEnv: Record | undefined; const trackingLauncher: SimulatorLauncher = async (_uuid, _bundleId, _executor, opts?) => { @@ -130,7 +131,7 @@ describe('launch_app_sim tool', () => { { simulatorId: 'test-uuid-123', bundleId: 'io.sentry.testapp', - args: ['--debug', '--verbose'], + launchArgs: ['--debug', '--verbose'], env: { STAGING_ENABLED: '1' }, }, installCheckExecutor, diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index 61341860..e6a0bbd9 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -66,7 +66,14 @@ const baseOptions = { ), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), derivedDataPath: z.string().optional(), - extraArgs: z.array(z.string()).optional(), + extraArgs: z + .array(z.string()) + .optional() + .describe('Additional xcodebuild/build-settings arguments (not app launch arguments)'), + launchArgs: z + .array(z.string()) + .optional() + .describe('Arguments passed to the launched app process on simulator runtime'), useLatestOS: z .boolean() .optional() @@ -439,7 +446,14 @@ export function createBuildRunSimExecutor( phase: 'launch-app', status: 'started', }); - const launchResult: LaunchWithLoggingResult = await launcher(simulatorId, bundleId, executor); + const launchOptions = + params.launchArgs === undefined ? undefined : { args: params.launchArgs }; + const launchResult: LaunchWithLoggingResult = await launcher( + simulatorId, + bundleId, + executor, + launchOptions, + ); if (!launchResult.success) { const errorMessage = launchResult.error ?? 'Failed to launch app'; return createBuildRunDomainResult({ diff --git a/src/mcp/tools/simulator/launch_app_sim.ts b/src/mcp/tools/simulator/launch_app_sim.ts index d82a7ee1..065958b4 100644 --- a/src/mcp/tools/simulator/launch_app_sim.ts +++ b/src/mcp/tools/simulator/launch_app_sim.ts @@ -36,7 +36,10 @@ const baseSchemaObject = z.object({ "Name of the simulator (e.g., 'iPhone 17'). Provide EITHER this OR simulatorId, not both", ), bundleId: z.string().describe('Bundle identifier of the app to launch'), - args: z.array(z.string()).optional().describe('Optional arguments to pass to the app'), + launchArgs: z + .array(z.string()) + .optional() + .describe('Arguments passed to the launched app process on simulator runtime'), env: z .record(z.string(), z.string()) .optional() @@ -49,7 +52,7 @@ const internalSchemaObject = z.object({ simulatorId: z.string(), simulatorName: z.string().optional(), bundleId: z.string(), - args: z.array(z.string()).optional(), + launchArgs: z.array(z.string()).optional(), env: z.record(z.string(), z.string()).optional(), }); @@ -129,7 +132,7 @@ export function createLaunchAppSimExecutor( try { const launchResult = await launcher(params.simulatorId, params.bundleId, executor, { - args: params.args, + args: params.launchArgs, env: params.env, }); diff --git a/src/utils/device-steps.ts b/src/utils/device-steps.ts index 0da0506b..5b6ae5df 100644 --- a/src/utils/device-steps.ts +++ b/src/utils/device-steps.ts @@ -40,7 +40,7 @@ export async function launchAppOnDevice( bundleId: string, executor: CommandExecutor, fileSystem: FileSystemExecutor, - opts?: { env?: Record }, + opts?: { env?: Record; args?: string[] }, ): Promise { log('info', `Launching app ${bundleId} on device ${deviceId}`); const tempJsonPath = join(fileSystem.tmpdir(), `launch-${Date.now()}.json`); @@ -64,6 +64,10 @@ export async function launchAppOnDevice( command.push(bundleId); + if (opts?.args?.length) { + command.push(...opts.args); + } + const result = await executor(command, 'Launch app on device', false); if (!result.success) { await fileSystem.rm(tempJsonPath, { force: true }).catch(() => {});