GitHub Issue: #101 - Support TEST_RUNNER_ prefixed env vars
Core Need: Enable conditional test behavior by passing TEST_RUNNER_ prefixed environment variables from MCP client configurations to xcodebuild test processes. This addresses the specific use case of disabling runsForEachTargetApplicationUIConfiguration for faster development testing.
From the xcodebuild man page:
TEST_RUNNER_<VAR> Set an environment variable whose name is prefixed
with TEST_RUNNER_ to have that variable passed, with
its prefix stripped, to all test runner processes
launched during a test action. For example,
TEST_RUNNER_Foo=Bar xcodebuild test ... sets the
environment variable Foo=Bar in the test runner's
environment.
Users want to configure their MCP server with TEST_RUNNER_ prefixed environment variables:
{
"mcpServers": {
"XcodeBuildMCP": {
"type": "stdio",
"command": "npx",
"args": ["-y", "xcodebuildmcp@latest"],
"env": {
"TEST_RUNNER_USE_DEV_MODE": "YES"
}
}
}
}And have tests that can conditionally execute based on these variables:
func testFoo() throws {
let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
guard useDevMode else {
XCTFail("Test requires USE_DEV_MODE to be true")
return
}
// Test logic here...
}- All Xcode commands flow through
executeXcodeBuildCommand()function - Generic
CommandExecutorinterface handles all command execution - Test tools exist for device/simulator/macOS platforms
- Zod schemas provide parameter validation and type safety
src/utils/CommandExecutor.ts- Command execution interfacesrc/utils/build-utils.ts- ContainsexecuteXcodeBuildCommandsrc/mcp/tools/device/test_device.ts- Device testing toolsrc/mcp/tools/simulator/test_sim.ts- Simulator testing toolsrc/mcp/tools/macos/test_macos.ts- macOS testing toolsrc/utils/test/index.ts- Shared test logic for simulator
-
Automatic Detection (❌ Rejected)
- Scan
process.envfor TEST_RUNNER_ variables and always pass them - Issue: Security risk of environment variable leakage
- Issue: Unpredictable behavior based on server environment
- Scan
-
Explicit Parameter (✅ Chosen)
- Add
testRunnerEnvparameter to test tools - Users explicitly specify which variables to pass
- Benefits: Secure, predictable, well-validated
- Add
-
Hybrid Approach (🤔 Future Enhancement)
- Both automatic + explicit with explicit overriding
- Issue: Adds complexity, deferred for future consideration
RepoPrompt Analysis: Comprehensive architectural plan emphasizing security, type safety, and integration with existing patterns.
Gemini Analysis: Confirmed explicit approach as optimal, highlighting:
- Security benefits of explicit allow-list approach
- Architectural soundness of extending CommandExecutor
- Recommendation for automatic prefix handling for better UX
- Security-First: Only explicitly provided variables are passed (no automatic process.env scanning)
- User Experience: Automatic prefix handling - users provide unprefixed keys
- Architecture: Extend execution layer generically for future extensibility
- Validation: Zod schema enforcement with proper type safety
Input (what users specify):
{
"testRunnerEnv": {
"USE_DEV_MODE": "YES",
"runsForEachTargetApplicationUIConfiguration": "NO"
}
}Output (what gets passed to xcodebuild):
TEST_RUNNER_USE_DEV_MODE=YES \
TEST_RUNNER_runsForEachTargetApplicationUIConfiguration=NO \
xcodebuild test ...Objective: Create reproduction test to validate issue and later prove fix works
- Create test in
example_projects/iOS/MCPTestthat checks for environment variable - Run current test tools to demonstrate limitation (test should fail)
- Document baseline behavior
Test Code Example:
func testEnvironmentVariablePassthrough() throws {
let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES"
guard useDevMode else {
XCTFail("Test requires USE_DEV_MODE=YES via TEST_RUNNER_USE_DEV_MODE")
return
}
XCTAssertTrue(true, "Environment variable successfully passed through")
}Objective: Extend CommandExecutor and build utilities to support environment variables
File: src/utils/CommandExecutor.ts
Changes:
- Add
CommandExecOptionstype for execution options - Update
CommandExecutortype signature to accept optional execution options
export type CommandExecOptions = {
cwd?: string;
env?: Record<string, string | undefined>;
};
export type CommandExecutor = (
args: string[],
description?: string,
quiet?: boolean,
opts?: CommandExecOptions
) => Promise<CommandResponse>;File: src/utils/execution/index.ts
Changes:
- Re-export
CommandExecOptionstype
export type { CommandExecutor, CommandResponse, CommandExecOptions } from '../CommandExecutor.js';File: src/utils/command.ts
Changes:
- Modify
getDefaultCommandExecutorto mergeopts.envwithprocess.envwhen spawning
// In the returned function:
const env = { ...process.env, ...(opts?.env ?? {}) };
// Pass env and opts?.cwd to spawn/exec callFile: src/utils/environment.ts
Changes:
- Add
normalizeTestRunnerEnvfunction
export function normalizeTestRunnerEnv(
userVars?: Record<string, string | undefined>
): Record<string, string> {
const result: Record<string, string> = {};
if (userVars) {
for (const [key, value] of Object.entries(userVars)) {
if (value !== undefined) {
result[`TEST_RUNNER_${key}`] = value;
}
}
}
return result;
}File: src/utils/build-utils.ts
Changes:
- Add optional
execOpts?: CommandExecOptionsparameter (6th parameter) - Pass execution options through to
CommandExecutorcalls
export async function executeXcodeBuildCommand(
build: { /* existing fields */ },
runtime: { /* existing fields */ },
preferXcodebuild = false,
action: 'build' | 'test' | 'archive' | 'analyze' | string,
executor: CommandExecutor = getDefaultCommandExecutor(),
execOpts?: CommandExecOptions, // NEW
): Promise<ToolResponse>Objective: Add testRunnerEnv parameter to all test tools and wire through execution
File: src/mcp/tools/device/test_device.ts
Changes:
- Add
testRunnerEnvto Zod schema with validation - Import and use
normalizeTestRunnerEnv - Pass execution options to
executeXcodeBuildCommand
Schema Addition:
testRunnerEnv: z
.record(z.string(), z.string().optional())
.optional()
.describe('Test runner environment variables (TEST_RUNNER_ prefix added automatically)')Usage:
const execEnv = normalizeTestRunnerEnv(params.testRunnerEnv);
const testResult = await executeXcodeBuildCommand(
{ /* build params */ },
{ /* runtime params */ },
params.preferXcodebuild ?? false,
'test',
executor,
{ env: execEnv } // NEW
);File: src/mcp/tools/macos/test_macos.ts
Changes: Same pattern as device test tool
- Schema addition for
testRunnerEnv - Import
normalizeTestRunnerEnv - Pass execution options to
executeXcodeBuildCommand
File: src/mcp/tools/simulator/test_sim.ts
Changes:
- Add
testRunnerEnvto schema - Pass through to
handleTestLogic
File: src/utils/test/index.ts
Changes:
- Update
handleTestLogicsignature to accepttestRunnerEnv?: Record<string, string | undefined> - Import and use
normalizeTestRunnerEnv - Pass execution options to
executeXcodeBuildCommand
Objective: Comprehensive testing coverage for new functionality
File: src/utils/__tests__/environment.test.ts
Tests:
- Test
normalizeTestRunnerEnvwith various inputs - Verify prefix addition
- Verify undefined filtering
- Verify empty input handling
Files: Update existing test files for test tools
Tests:
- Verify
testRunnerEnvparameter is properly validated - Verify environment variables are passed through
CommandExecutor - Mock executor to verify correct env object construction
Files: Test files in each tool directory
Tests:
- Verify schema exports include new
testRunnerEnvfield - Verify parameter typing is correct
Objective: Prove the fix works with real xcodebuild scenarios
Tasks:
- Run reproduction test from Phase 0 with new
testRunnerEnvparameter - Verify test passes (proving env var was successfully passed)
- Document the before/after behavior
Tasks:
- Test with actual iOS project using
runsForEachTargetApplicationUIConfiguration - Verify performance difference when variable is set
- Test with multiple environment variables
- Test edge cases (empty values, special characters)
- No Environment Leakage: Only explicit user-provided variables are passed
- Command Injection Prevention: Environment variables passed as separate object, not interpolated into command string
- Input Validation: Zod schemas prevent malformed inputs
- Prefix Enforcement: Only TEST_RUNNER_ prefixed variables can be set
- Never log environment variable values (keys only for debugging)
- Filter out undefined values to prevent accidental exposure
- Validate all user inputs through Zod schemas
- Document supported TEST_RUNNER_ variables from Apple's documentation
- Extends existing
CommandExecutorpattern generically - Maintains backward compatibility (all existing calls remain valid)
- Follows established Zod validation patterns
- Consistent API across all test tools
CommandExecOptionscan support additional execution options (timeout, cwd, etc.)- Pattern can be extended to other tools that need environment variables
- Generic approach allows for non-TEST_RUNNER_ use cases in the future
src/utils/__tests__/environment.test.ts- Unit tests for environment utilities
src/utils/CommandExecutor.ts- Add execution options typessrc/utils/execution/index.ts- Re-export new typessrc/utils/command.ts- Update default executor to handle envsrc/utils/environment.ts- AddnormalizeTestRunnerEnvutilitysrc/utils/build-utils.ts- UpdateexecuteXcodeBuildCommandsignaturesrc/mcp/tools/device/test_device.ts- Add schema and integrationsrc/mcp/tools/macos/test_macos.ts- Add schema and integrationsrc/mcp/tools/simulator/test_sim.ts- Add schema and pass-throughsrc/utils/test/index.ts- UpdatehandleTestLogicfor simulator path- Test files for each modified tool - Add validation tests
- Functionality: Users can pass
testRunnerEnvparameter to test tools and have variables appear in test runner environment - Security: No unintended environment variable leakage from server process
- Usability: Users specify unprefixed variable names for better UX
- Compatibility: All existing test tool calls continue to work unchanged
- Validation: Comprehensive test coverage proves the feature works end-to-end
- Configuration Profiles: Allow users to define common TEST_RUNNER_ variable sets in config files
- Variable Discovery: Help users discover available TEST_RUNNER_ variables
- Build Tool Support: Extend to build tools if Apple adds similar BUILD_RUNNER_ support
- Performance Monitoring: Track impact of environment variable passing on build times
- Phase 0: 1-2 hours (reproduction test setup)
- Phase 1: 4-6 hours (infrastructure changes)
- Phase 2: 3-4 hours (tool integration)
- Phase 3: 4-5 hours (testing)
- Phase 4: 2-3 hours (validation)
Total Estimated Time: 14-20 hours
This implementation plan provides a secure, user-friendly, and architecturally sound solution for TEST_RUNNER_ environment variable support. The explicit parameter approach with automatic prefix handling balances security concerns with user experience, while the test-driven development approach ensures we can prove the solution works as intended.
The plan leverages XcodeBuildMCP's existing patterns and provides a foundation for future environment variable needs across the tool ecosystem.