Skip to content

config.yaml is unreliable with MCP clients — env vars should be the primary configuration surface #267

@ichoosetoaccept

Description

@ichoosetoaccept

Problem

XcodeBuildMCP's primary configuration mechanism is a project-local file at .xcodebuildmcp/config.yaml. This file is discovered relative to process.cwd(). However, MCP clients do not guarantee that process.cwd() points to the user's project directory when spawning stdio-based MCP servers.

Empirically verified with Windsurf (Codeium):

process.cwd(): /
process.argv: ["node", ".../build/cli.js", "mcp"]

The server starts with cwd=/, so loadProjectConfig never finds <project>/.xcodebuildmcp/config.yaml. This means:

  • enabledWorkflows is empty, so only defaultEnabled workflows are registered (simulator + session-management). macOS, device, coverage, debugging, logging, ui-automation, xcode-ide, and other workflows are invisible.
  • sessionDefaults from the config file are not applied.
  • incrementalBuildsEnabled, debug, and other flags are not set.

The user has no way to fix this without either:

  1. Hardcoding a --cwd path in the MCP config (breaks multi-project setups)
  2. Using XCODEBUILDMCP_CWD env var (same problem)

Why cwd is unreliable for MCP servers

The MCP specification does not prescribe the working directory of spawned server processes. While all three official MCP SDKs support cwd as a parameter, MCP clients vary widely in whether they actually set it:

MCP Client cwd behavior
VS Code Supports ${workspaceFolder} interpolation in mcp.json
Cursor Supports ${workspaceFolder} interpolation (added after community requests)
Windsurf Always sets cwd=/reported here
OpenAI Codex Does not set workspace cwdopenai/codex#4222
Claude Desktop No cwd field in config; relies on absolute paths

Relying on process.cwd() + a config file is fragile because:

  • MCP clients don't guarantee cwd. Windsurf sets it to /. Codex has the same bug. Claude Desktop doesn't expose it at all.
  • ${workspaceFolder} interpolation is not standard. Only VS Code and Cursor support it. Windsurf and Claude Desktop do not.
  • roots capability could theoretically solve this, but client adoption is sparse (very few clients support it today).
  • tools/listChanged notifications could allow lazy tool registration after config is discovered, but not all clients honor these notifications in practice.

Env vars are the ecosystem standard, not "legacy"

The current CONFIGURATION.md labels environment variables as "legacy." This is at odds with the broader MCP ecosystem, where env vars are the de facto standard for server configuration.

Evidence:

  1. The MCP config schema itself is designed around env vars. Every MCP client implements the env field:

    {
      "mcpServers": {
        "my-server": {
          "command": "my-server",
          "args": [],
          "env": {
            "MY_SERVER_API_KEY": "...",
            "MY_SERVER_OPTION": "true"
          }
        }
      }
    }
  2. MCP best practices (modelcontextprotocol.info) explicitly recommends: "Externalize all configuration with environment-specific overrides" — using Pydantic BaseSettings with env_prefix as the reference pattern.

  3. Reference MCP server implementations (filesystem, GitHub, Sentry, Playwright, etc.) all use environment variables for configuration, not config files.

  4. Every MCP client supports env (Claude Desktop, VS Code, Cursor, Windsurf, Claude Code, Codex). It works regardless of cwd, doesn't require filesystem discovery, and is explicit.

  5. 12-factor app principles — widely adopted in the server ecosystem — prescribe that config should live in the environment, not in files tied to deployment.

Config files have their place (CLI usage, complex multi-profile setups), but env vars should be the recommended path for MCP client integration since they work reliably across all clients.

Current env var support

XcodeBuildMCP already supports env vars for most config options via readEnvConfig() in config-store.ts:

Config key Env var Status
enabledWorkflows XCODEBUILDMCP_ENABLED_WORKFLOWS Supported
debug XCODEBUILDMCP_DEBUG Supported
sentryDisabled XCODEBUILDMCP_SENTRY_DISABLED Supported
incrementalBuildsEnabled INCREMENTAL_BUILDS_ENABLED Supported
experimentalWorkflowDiscovery XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY Supported
disableSessionDefaults XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS Supported
disableXcodeAutoSync XCODEBUILDMCP_DISABLE_XCODE_AUTO_SYNC Supported
debuggerBackend XCODEBUILDMCP_DEBUGGER_BACKEND Supported
sessionDefaults.* Not supported

The gap is sessionDefaults — there are no env vars for workspacePath, scheme, platform, derivedDataPath, suppressWarnings, etc.

Proposal

  1. Remove the "legacy" label from environment variables in the documentation. Env vars are the ecosystem standard for MCP server configuration, not a deprecated mechanism.

  2. Add env vars for session defaults (the main gap):

    • XCODEBUILDMCP_WORKSPACE_PATH
    • XCODEBUILDMCP_PROJECT_PATH
    • XCODEBUILDMCP_SCHEME
    • XCODEBUILDMCP_PLATFORM
    • XCODEBUILDMCP_DERIVED_DATA_PATH
    • XCODEBUILDMCP_SUPPRESS_WARNINGS
    • XCODEBUILDMCP_SIMULATOR_NAME
    • XCODEBUILDMCP_SIMULATOR_ID
    • XCODEBUILDMCP_CONFIGURATION
    • XCODEBUILDMCP_USE_LATEST_OS
    • XCODEBUILDMCP_PREFER_XCODEBUILD
    • XCODEBUILDMCP_ARCH
    • XCODEBUILDMCP_BUNDLE_ID
    • XCODEBUILDMCP_DEVICE_ID
    • XCODEBUILDMCP_SIMULATOR_PLATFORM
  3. Document env vars as the recommended configuration method for MCP client integration. Provide copy-pastable MCP config snippets for common setups (iOS, macOS, multi-platform) so users can get started without guessing which workflows and session defaults they need.

  4. Have xcodebuildmcp setup output a ready-to-paste MCP config JSON block (e.g. --format mcp-json). The interactive flow is genuinely useful for guiding users through configuration — the output format just needs to match how MCP clients consume it.

  5. Be explicit about the configuration layering. Env vars and config.yaml are not competing sources of truth — they serve different contexts with clear precedence:

    • Env vars → MCP client integration (set in mcp_config.json's env field)
    • Config file → project-local config (committed to repo, used by CLI / xcodebuildmcp setup)
    • session_set_defaults tool → agent runtime overrides (set during a session)

    Precedence: tool > file > env. This is the standard pattern (like git config --global < --local < --flag). Document it clearly so users understand which layer wins.

Accompanying PR

A PR implementing all of the above is ready:

  • readEnvSessionDefaults() in config-store.ts — parses all 15 session default env vars
  • resolveSessionDefaults() updated — merges env → file → tool overrides
  • selectionToMcpConfigJson() in setup.tsxcodebuildmcp setup --format mcp-json
  • CONFIGURATION.md rewritten — env vars first, "legacy" label removed, layering documented
  • Tests for env var parsing and file-over-env precedence
  • Test for --format mcp-json output

Example: full env-var-based MCP config

This replaces a config.yaml entirely:

{
  "mcpServers": {
    "XcodeBuildMCP": {
      "command": "xcodebuildmcp",
      "args": ["mcp"],
      "env": {
        "XCODEBUILDMCP_ENABLED_WORKFLOWS": "coverage,debugging,doctor,logging,macos,project-discovery,project-scaffolding,swift-package,ui-automation,utilities,xcode-ide",
        "XCODEBUILDMCP_DEBUG": "true",
        "INCREMENTAL_BUILDS_ENABLED": "true",
        "XCODEBUILDMCP_WORKSPACE_PATH": "/Users/me/myproject/myproject.xcworkspace",
        "XCODEBUILDMCP_SCHEME": "myproject",
        "XCODEBUILDMCP_PLATFORM": "macOS",
        "XCODEBUILDMCP_DERIVED_DATA_PATH": "/Users/me/myproject/.derivedData",
        "XCODEBUILDMCP_SUPPRESS_WARNINGS": "true"
      }
    }
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions