Skip to content

Commit d2a2f70

Browse files
author
Ismar Iljazovic
committed
feat: env var session defaults and setup --format mcp-json
- Add readEnvSessionDefaults() to parse all 15 session default env vars (XCODEBUILDMCP_WORKSPACE_PATH, XCODEBUILDMCP_SCHEME, etc.) - Wire env layer into resolveSessionDefaults() with precedence: tool overrides > config file > env vars - Add --format mcp-json flag to xcodebuildmcp setup: outputs a ready-to-paste MCP client config JSON block instead of writing config.yaml - Update CONFIGURATION.md: env vars documented as recommended method for MCP client integration, remove 'legacy' label, add layering section - Add tests for env var parsing and file-over-env precedence - Add test for --format mcp-json output Closes #267
1 parent 684f71e commit d2a2f70

File tree

6 files changed

+382
-37
lines changed

6 files changed

+382
-37
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## [Unreleased]
4+
5+
### Added
6+
7+
- Added environment variable support for all session defaults (e.g., `XCODEBUILDMCP_WORKSPACE_PATH`, `XCODEBUILDMCP_SCHEME`, `XCODEBUILDMCP_PLATFORM`), enabling full configuration via the MCP client `env` field without requiring a config file.
8+
- Added `--format mcp-json` flag to `xcodebuildmcp setup` that outputs a ready-to-paste MCP client config JSON block instead of writing `config.yaml`.
9+
10+
### Changed
11+
12+
- Environment variables are now documented as the recommended configuration method for MCP client integration, replacing the previous "legacy" designation. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md).
13+
314
## [2.2.1]
415

516
- Fix AXe bundling issue.

docs/CONFIGURATION.md

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Configuration
22

3-
XcodeBuildMCP reads configuration from a project config file. The config file is optional but provides deterministic, repo-scoped behavior for every session.
3+
XcodeBuildMCP reads configuration from environment variables and/or a project config file. Both are optional but provide deterministic behavior for every session.
44

55
## Contents
66

7+
- [Environment variables](#environment-variables)
78
- [Config file](#config-file)
9+
- [Configuration layering](#configuration-layering)
810
- [Session defaults](#session-defaults)
911
- [Workflow selection](#workflow-selection)
1012
- [Build settings](#build-settings)
@@ -13,13 +15,87 @@ XcodeBuildMCP reads configuration from a project config file. The config file is
1315
- [Templates](#templates)
1416
- [Telemetry](#telemetry)
1517
- [Quick reference](#quick-reference)
16-
- [Environment variables (legacy)](#environment-variables-legacy)
18+
19+
---
20+
21+
## Environment variables
22+
23+
Environment variables are the recommended configuration method for MCP client integration. Set them in the `env` field of your MCP client config (e.g., `mcp_config.json` for Windsurf, `.vscode/mcp.json` for VS Code, `claude_desktop_config.json` for Claude Desktop).
24+
25+
This approach works reliably across all MCP clients regardless of working directory, and avoids the need for filesystem-based config discovery.
26+
27+
### General settings
28+
29+
| Config option | Environment variable |
30+
|---------------|---------------------|
31+
| `enabledWorkflows` | `XCODEBUILDMCP_ENABLED_WORKFLOWS` (comma-separated) |
32+
| `experimentalWorkflowDiscovery` | `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY` |
33+
| `disableSessionDefaults` | `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS` |
34+
| `disableXcodeAutoSync` | `XCODEBUILDMCP_DISABLE_XCODE_AUTO_SYNC` |
35+
| `incrementalBuildsEnabled` | `INCREMENTAL_BUILDS_ENABLED` |
36+
| `debug` | `XCODEBUILDMCP_DEBUG` |
37+
| `sentryDisabled` | `XCODEBUILDMCP_SENTRY_DISABLED` |
38+
| `debuggerBackend` | `XCODEBUILDMCP_DEBUGGER_BACKEND` |
39+
| `dapRequestTimeoutMs` | `XCODEBUILDMCP_DAP_REQUEST_TIMEOUT_MS` |
40+
| `dapLogEvents` | `XCODEBUILDMCP_DAP_LOG_EVENTS` |
41+
| `launchJsonWaitMs` | `XBMCP_LAUNCH_JSON_WAIT_MS` |
42+
| `uiDebuggerGuardMode` | `XCODEBUILDMCP_UI_DEBUGGER_GUARD_MODE` |
43+
| `axePath` | `XCODEBUILDMCP_AXE_PATH` |
44+
| `iosTemplatePath` | `XCODEBUILDMCP_IOS_TEMPLATE_PATH` |
45+
| `iosTemplateVersion` | `XCODEBUILD_MCP_IOS_TEMPLATE_VERSION` |
46+
| `macosTemplatePath` | `XCODEBUILDMCP_MACOS_TEMPLATE_PATH` |
47+
| `macosTemplateVersion` | `XCODEBUILD_MCP_MACOS_TEMPLATE_VERSION` |
48+
49+
### Session default settings
50+
51+
| Session default | Environment variable |
52+
|----------------|---------------------|
53+
| `workspacePath` | `XCODEBUILDMCP_WORKSPACE_PATH` |
54+
| `projectPath` | `XCODEBUILDMCP_PROJECT_PATH` |
55+
| `scheme` | `XCODEBUILDMCP_SCHEME` |
56+
| `configuration` | `XCODEBUILDMCP_CONFIGURATION` |
57+
| `simulatorName` | `XCODEBUILDMCP_SIMULATOR_NAME` |
58+
| `simulatorId` | `XCODEBUILDMCP_SIMULATOR_ID` |
59+
| `simulatorPlatform` | `XCODEBUILDMCP_SIMULATOR_PLATFORM` |
60+
| `deviceId` | `XCODEBUILDMCP_DEVICE_ID` |
61+
| `platform` | `XCODEBUILDMCP_PLATFORM` |
62+
| `useLatestOS` | `XCODEBUILDMCP_USE_LATEST_OS` |
63+
| `arch` | `XCODEBUILDMCP_ARCH` |
64+
| `suppressWarnings` | `XCODEBUILDMCP_SUPPRESS_WARNINGS` |
65+
| `derivedDataPath` | `XCODEBUILDMCP_DERIVED_DATA_PATH` |
66+
| `preferXcodebuild` | `XCODEBUILDMCP_PREFER_XCODEBUILD` |
67+
| `bundleId` | `XCODEBUILDMCP_BUNDLE_ID` |
68+
69+
### Example MCP config
70+
71+
```json
72+
{
73+
"mcpServers": {
74+
"XcodeBuildMCP": {
75+
"command": "npx",
76+
"args": ["-y", "xcodebuildmcp@latest", "mcp"],
77+
"env": {
78+
"XCODEBUILDMCP_ENABLED_WORKFLOWS": "simulator,macos,debugging,logging",
79+
"XCODEBUILDMCP_WORKSPACE_PATH": "/Users/me/MyApp/MyApp.xcworkspace",
80+
"XCODEBUILDMCP_SCHEME": "MyApp",
81+
"XCODEBUILDMCP_PLATFORM": "macOS"
82+
}
83+
}
84+
}
85+
}
86+
```
87+
88+
You can also generate this block interactively:
89+
90+
```bash
91+
xcodebuildmcp setup --format mcp-json
92+
```
1793

1894
---
1995

2096
## Config file
2197

22-
Create a config file at your workspace root:
98+
The config file provides repo-scoped, version-controllable configuration. Create it at your workspace root:
2399

24100
```
25101
<workspace-root>/.xcodebuildmcp/config.yaml
@@ -337,30 +413,19 @@ Notes:
337413

338414
---
339415

340-
## Environment variables (legacy)
416+
## Configuration layering
341417

342-
Environment variables are supported for backwards compatibility but the config file is preferred.
418+
When multiple configuration sources are present, they are merged with clear precedence:
343419

344-
| Config option | Environment variable |
345-
|---------------|---------------------|
346-
| `enabledWorkflows` | `XCODEBUILDMCP_ENABLED_WORKFLOWS` (comma-separated) |
347-
| `experimentalWorkflowDiscovery` | `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY` |
348-
| `disableSessionDefaults` | `XCODEBUILDMCP_DISABLE_SESSION_DEFAULTS` |
349-
| `incrementalBuildsEnabled` | `INCREMENTAL_BUILDS_ENABLED` |
350-
| `debug` | `XCODEBUILDMCP_DEBUG` |
351-
| `sentryDisabled` | `XCODEBUILDMCP_SENTRY_DISABLED` |
352-
| `debuggerBackend` | `XCODEBUILDMCP_DEBUGGER_BACKEND` |
353-
| `dapRequestTimeoutMs` | `XCODEBUILDMCP_DAP_REQUEST_TIMEOUT_MS` |
354-
| `dapLogEvents` | `XCODEBUILDMCP_DAP_LOG_EVENTS` |
355-
| `launchJsonWaitMs` | `XBMCP_LAUNCH_JSON_WAIT_MS` |
356-
| `uiDebuggerGuardMode` | `XCODEBUILDMCP_UI_DEBUGGER_GUARD_MODE` |
357-
| `axePath` | `XCODEBUILDMCP_AXE_PATH` |
358-
| `iosTemplatePath` | `XCODEBUILDMCP_IOS_TEMPLATE_PATH` |
359-
| `iosTemplateVersion` | `XCODEBUILD_MCP_IOS_TEMPLATE_VERSION` |
360-
| `macosTemplatePath` | `XCODEBUILDMCP_MACOS_TEMPLATE_PATH` |
361-
| `macosTemplateVersion` | `XCODEBUILD_MCP_MACOS_TEMPLATE_VERSION` |
420+
1. **`session_set_defaults` tool** (highest) — agent runtime overrides, set during a session
421+
2. **Config file** — project-local config (`config.yaml`), committed to repo
422+
3. **Environment variables** (lowest) — MCP client integration, set in `mcp_config.json`
423+
424+
This follows the same pattern as tools like `git config` (`--flag` > `--local` > `--global`). Each layer serves a different context:
362425

363-
Config file takes precedence over environment variables when both are set.
426+
- **Env vars** are the portable MCP client integration path — they work regardless of working directory and are supported by every MCP client.
427+
- **Config file** is for repo-scoped, version-controlled settings and interactive CLI usage.
428+
- **Tool calls** are for agent-driven runtime adjustments.
364429

365430
---
366431

src/cli/commands/__tests__/setup.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,91 @@ describe('setup command', () => {
248248
).rejects.toThrow('Setup prerequisites failed');
249249
});
250250

251+
it('outputs MCP config JSON when format is mcp-json', async () => {
252+
const fs = createMockFileSystemExecutor({
253+
existsSync: () => false,
254+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
255+
readdir: async (targetPath) => {
256+
if (targetPath === cwd) {
257+
return [
258+
{
259+
name: 'App.xcworkspace',
260+
isDirectory: () => true,
261+
isSymbolicLink: () => false,
262+
},
263+
];
264+
}
265+
266+
return [];
267+
},
268+
readFile: async () => '',
269+
writeFile: async () => {},
270+
});
271+
272+
const executor: CommandExecutor = async (command) => {
273+
if (command.includes('--json')) {
274+
return createMockCommandResponse({
275+
success: true,
276+
output: JSON.stringify({
277+
devices: {
278+
'iOS 17.0': [
279+
{
280+
name: 'iPhone 15',
281+
udid: 'SIM-1',
282+
state: 'Shutdown',
283+
isAvailable: true,
284+
},
285+
],
286+
},
287+
}),
288+
});
289+
}
290+
291+
if (command[0] === 'xcrun') {
292+
return createMockCommandResponse({
293+
success: true,
294+
output: `== Devices ==\n-- iOS 17.0 --\n iPhone 15 (SIM-1) (Shutdown)`,
295+
});
296+
}
297+
298+
return createMockCommandResponse({
299+
success: true,
300+
output: `Information about workspace "App":\n Schemes:\n App`,
301+
});
302+
};
303+
304+
const result = await runSetupWizard({
305+
cwd,
306+
fs,
307+
executor,
308+
prompter: createTestPrompter(),
309+
quietOutput: true,
310+
outputFormat: 'mcp-json',
311+
});
312+
313+
expect(result.configPath).toBeUndefined();
314+
expect(result.mcpConfigJson).toBeDefined();
315+
316+
const parsed = JSON.parse(result.mcpConfigJson!) as {
317+
mcpServers: {
318+
XcodeBuildMCP: {
319+
command: string;
320+
args: string[];
321+
env: Record<string, string>;
322+
};
323+
};
324+
};
325+
326+
const serverConfig = parsed.mcpServers.XcodeBuildMCP;
327+
expect(serverConfig.command).toBe('npx');
328+
expect(serverConfig.args).toEqual(['-y', 'xcodebuildmcp@latest', 'mcp']);
329+
expect(serverConfig.env.XCODEBUILDMCP_ENABLED_WORKFLOWS).toBeDefined();
330+
expect(serverConfig.env.XCODEBUILDMCP_WORKSPACE_PATH).toBe(path.join(cwd, 'App.xcworkspace'));
331+
expect(serverConfig.env.XCODEBUILDMCP_SCHEME).toBe('App');
332+
expect(serverConfig.env.XCODEBUILDMCP_SIMULATOR_ID).toBe('SIM-1');
333+
expect(serverConfig.env.XCODEBUILDMCP_SIMULATOR_NAME).toBe('iPhone 15');
334+
});
335+
251336
it('fails in non-interactive mode', async () => {
252337
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
253338
Object.defineProperty(process.stdout, 'isTTY', { value: false, configurable: true });

0 commit comments

Comments
 (0)