Skip to content

Commit b23e798

Browse files
author
Ismar Iljazovic
committed
Add platform-aware setup wizard and mcp-json output
Introduce platform selection to the setup wizard, recommend workflows per platform, and avoid prompting for a simulator when macOS is the only platform selected. Add helpers and constants (SetupPlatform, PLATFORM_WORKFLOWS, PLATFORM_OPTIONS, infer/derive/filter helpers), a multi-select platform prompt, and make the setup flow platform-aware (seed workflow defaults, filter simulators, preserve platform in sessionDefaults). Add selectionToMcpConfigJson() and a --format mcp-json option to print a ready-to-paste MCP client config JSON block (runSetupWizard supports 'mcp-json' early-exit). Update tests (createPlatformPrompter and four platform-aware cases) and CHANGELOG.md to document the new behavior.
1 parent b7f8a89 commit b23e798

3 files changed

Lines changed: 467 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,15 @@
66

77
- Added environment variable support for session defaults (e.g. `XCODEBUILDMCP_WORKSPACE_PATH`, `XCODEBUILDMCP_SCHEME`, `XCODEBUILDMCP_PLATFORM`) so MCP clients can supply startup defaults in their config without a project config file ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)). See [docs/CONFIGURATION.md](docs/CONFIGURATION.md#environment-variables).
88
- Added `--format mcp-json` flag to `xcodebuildmcp setup` that exports an env-based MCP client config block instead of writing `config.yaml` ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)).
9+
- Added platform selection step to the `xcodebuildmcp setup` wizard. You now choose which platforms you are developing for (macOS, iOS, tvOS, watchOS, visionOS) before selecting workflows. Based on the selection, the wizard automatically recommends the appropriate workflow set.
910

1011
### Changed
1112

1213
- Clarified configuration layering: `session_set_defaults` overrides `config.yaml`, which overrides environment variables. See [docs/CONFIGURATION.md](docs/CONFIGURATION.md) ([#268](https://github.com/getsentry/XcodeBuildMCP/pull/268) by [@detailobsessed](https://github.com/detailobsessed)).
1314
- Improved `xcodebuildmcp setup` reliability when optional targets (like physical devices) are unavailable.
15+
- The `setup` wizard no longer prompts for a simulator when macOS is the only selected platform — macOS apps run natively and do not require a simulator.
16+
- When a single platform is selected, `xcodebuildmcp setup` now writes `platform` to `sessionDefaults` in `config.yaml` and includes `XCODEBUILDMCP_PLATFORM` in `--format mcp-json` output. For multi-platform projects the platform key is omitted so the agent can choose per-command.
17+
- The `setup` wizard remembers previous choices on re-run: existing `config.yaml` values (including the new `platform`) are pre-loaded as defaults for every prompt.
1418

1519
### Fixed
1620

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

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,26 @@ function createTestPrompter(): Prompter {
108108
};
109109
}
110110

111+
function createPlatformPrompter(platforms: string[]): Prompter {
112+
let selectManyCalls = 0;
113+
return {
114+
selectOne: async <T>(opts: { options: Array<{ value: T }> }) => {
115+
const preferredOption = opts.options.find((option) => option.value != null);
116+
return (preferredOption ?? opts.options[0]).value;
117+
},
118+
selectMany: async <T>(opts: { options: Array<{ value: T }> }) => {
119+
selectManyCalls++;
120+
if (selectManyCalls === 1) {
121+
return opts.options
122+
.filter((option) => platforms.includes(String(option.value)))
123+
.map((option) => option.value);
124+
}
125+
return opts.options.map((option) => option.value);
126+
},
127+
confirm: async (opts: { defaultValue: boolean }) => opts.defaultValue,
128+
};
129+
}
130+
111131
describe('setup command', () => {
112132
const originalStdinIsTTY = process.stdin.isTTY;
113133
const originalStdoutIsTTY = process.stdout.isTTY;
@@ -1054,4 +1074,230 @@ sessionDefaults:
10541074

10551075
await expect(runSetupWizard()).rejects.toThrow('requires an interactive TTY');
10561076
});
1077+
1078+
it('skips simulator and sets platform for macOS-only selection', async () => {
1079+
let storedConfig = '';
1080+
1081+
const fs = createMockFileSystemExecutor({
1082+
existsSync: (targetPath) => targetPath === configPath && storedConfig.length > 0,
1083+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1084+
readdir: async (targetPath) => {
1085+
if (targetPath === cwd) {
1086+
return [
1087+
{
1088+
name: 'App.xcworkspace',
1089+
isDirectory: () => true,
1090+
isSymbolicLink: () => false,
1091+
},
1092+
];
1093+
}
1094+
return [];
1095+
},
1096+
readFile: async (targetPath) => {
1097+
if (targetPath !== configPath) throw new Error(`Unexpected read path: ${targetPath}`);
1098+
return storedConfig;
1099+
},
1100+
writeFile: async (targetPath, content) => {
1101+
if (targetPath !== configPath) throw new Error(`Unexpected write path: ${targetPath}`);
1102+
storedConfig = content;
1103+
},
1104+
});
1105+
1106+
const executor: CommandExecutor = async () =>
1107+
createMockCommandResponse({
1108+
success: true,
1109+
output: `Information about workspace "App":\n Schemes:\n App`,
1110+
});
1111+
1112+
await runSetupWizard({
1113+
cwd,
1114+
fs,
1115+
executor,
1116+
prompter: createPlatformPrompter(['macOS']),
1117+
quietOutput: true,
1118+
});
1119+
1120+
const parsed = parseYaml(storedConfig) as {
1121+
sessionDefaults?: Record<string, unknown>;
1122+
};
1123+
1124+
expect(parsed.sessionDefaults?.platform).toBe('macOS');
1125+
expect(parsed.sessionDefaults?.simulatorId).toBeUndefined();
1126+
expect(parsed.sessionDefaults?.simulatorName).toBeUndefined();
1127+
});
1128+
1129+
it('outputs XCODEBUILDMCP_PLATFORM=macOS and no simulator fields for macOS-only mcp-json', async () => {
1130+
const fs = createMockFileSystemExecutor({
1131+
existsSync: () => false,
1132+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1133+
readdir: async (targetPath) => {
1134+
if (targetPath === cwd) {
1135+
return [
1136+
{
1137+
name: 'App.xcworkspace',
1138+
isDirectory: () => true,
1139+
isSymbolicLink: () => false,
1140+
},
1141+
];
1142+
}
1143+
return [];
1144+
},
1145+
readFile: async () => '',
1146+
writeFile: async () => {},
1147+
});
1148+
1149+
const executor: CommandExecutor = async () =>
1150+
createMockCommandResponse({
1151+
success: true,
1152+
output: `Information about workspace "App":\n Schemes:\n App`,
1153+
});
1154+
1155+
const result = await runSetupWizard({
1156+
cwd,
1157+
fs,
1158+
executor,
1159+
prompter: createPlatformPrompter(['macOS']),
1160+
quietOutput: true,
1161+
outputFormat: 'mcp-json',
1162+
});
1163+
1164+
expect(result.mcpConfigJson).toBeDefined();
1165+
const parsed = JSON.parse(result.mcpConfigJson!) as {
1166+
mcpServers: { XcodeBuildMCP: { env: Record<string, string> } };
1167+
};
1168+
const env = parsed.mcpServers.XcodeBuildMCP.env;
1169+
1170+
expect(env.XCODEBUILDMCP_PLATFORM).toBe('macOS');
1171+
expect(env.XCODEBUILDMCP_SIMULATOR_ID).toBeUndefined();
1172+
expect(env.XCODEBUILDMCP_SIMULATOR_NAME).toBeUndefined();
1173+
});
1174+
1175+
it('outputs XCODEBUILDMCP_PLATFORM=iOS Simulator and simulator fields for iOS-only mcp-json', async () => {
1176+
const fs = createMockFileSystemExecutor({
1177+
existsSync: () => false,
1178+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1179+
readdir: async (targetPath) => {
1180+
if (targetPath === cwd) {
1181+
return [
1182+
{
1183+
name: 'App.xcworkspace',
1184+
isDirectory: () => true,
1185+
isSymbolicLink: () => false,
1186+
},
1187+
];
1188+
}
1189+
return [];
1190+
},
1191+
readFile: async () => '',
1192+
writeFile: async () => {},
1193+
});
1194+
1195+
const executor: CommandExecutor = async (command) => {
1196+
if (command.includes('--json')) {
1197+
return createMockCommandResponse({
1198+
success: true,
1199+
output: JSON.stringify({
1200+
devices: {
1201+
'iOS 17.0': [
1202+
{ name: 'iPhone 15', udid: 'SIM-1', state: 'Shutdown', isAvailable: true },
1203+
],
1204+
},
1205+
}),
1206+
});
1207+
}
1208+
if (command[0] === 'xcrun') {
1209+
return createMockCommandResponse({
1210+
success: true,
1211+
output: `== Devices ==\n-- iOS 17.0 --\n iPhone 15 (SIM-1) (Shutdown)`,
1212+
});
1213+
}
1214+
return createMockCommandResponse({
1215+
success: true,
1216+
output: `Information about workspace "App":\n Schemes:\n App`,
1217+
});
1218+
};
1219+
1220+
const result = await runSetupWizard({
1221+
cwd,
1222+
fs,
1223+
executor,
1224+
prompter: createPlatformPrompter(['iOS']),
1225+
quietOutput: true,
1226+
outputFormat: 'mcp-json',
1227+
});
1228+
1229+
expect(result.mcpConfigJson).toBeDefined();
1230+
const parsed = JSON.parse(result.mcpConfigJson!) as {
1231+
mcpServers: { XcodeBuildMCP: { env: Record<string, string> } };
1232+
};
1233+
const env = parsed.mcpServers.XcodeBuildMCP.env;
1234+
1235+
expect(env.XCODEBUILDMCP_PLATFORM).toBe('iOS Simulator');
1236+
expect(env.XCODEBUILDMCP_SIMULATOR_ID).toBe('SIM-1');
1237+
expect(env.XCODEBUILDMCP_SIMULATOR_NAME).toBe('iPhone 15');
1238+
});
1239+
1240+
it('omits XCODEBUILDMCP_PLATFORM for multi-platform mcp-json', async () => {
1241+
const fs = createMockFileSystemExecutor({
1242+
existsSync: () => false,
1243+
stat: async () => ({ isDirectory: () => true, mtimeMs: 0 }),
1244+
readdir: async (targetPath) => {
1245+
if (targetPath === cwd) {
1246+
return [
1247+
{
1248+
name: 'App.xcworkspace',
1249+
isDirectory: () => true,
1250+
isSymbolicLink: () => false,
1251+
},
1252+
];
1253+
}
1254+
return [];
1255+
},
1256+
readFile: async () => '',
1257+
writeFile: async () => {},
1258+
});
1259+
1260+
const executor: CommandExecutor = async (command) => {
1261+
if (command.includes('--json')) {
1262+
return createMockCommandResponse({
1263+
success: true,
1264+
output: JSON.stringify({
1265+
devices: {
1266+
'iOS 17.0': [
1267+
{ name: 'iPhone 15', udid: 'SIM-1', state: 'Shutdown', isAvailable: true },
1268+
],
1269+
},
1270+
}),
1271+
});
1272+
}
1273+
if (command[0] === 'xcrun') {
1274+
return createMockCommandResponse({
1275+
success: true,
1276+
output: `== Devices ==\n-- iOS 17.0 --\n iPhone 15 (SIM-1) (Shutdown)`,
1277+
});
1278+
}
1279+
return createMockCommandResponse({
1280+
success: true,
1281+
output: `Information about workspace "App":\n Schemes:\n App`,
1282+
});
1283+
};
1284+
1285+
const result = await runSetupWizard({
1286+
cwd,
1287+
fs,
1288+
executor,
1289+
prompter: createPlatformPrompter(['macOS', 'iOS']),
1290+
quietOutput: true,
1291+
outputFormat: 'mcp-json',
1292+
});
1293+
1294+
expect(result.mcpConfigJson).toBeDefined();
1295+
const parsed = JSON.parse(result.mcpConfigJson!) as {
1296+
mcpServers: { XcodeBuildMCP: { env: Record<string, string> } };
1297+
};
1298+
const env = parsed.mcpServers.XcodeBuildMCP.env;
1299+
1300+
expect(env.XCODEBUILDMCP_PLATFORM).toBeUndefined();
1301+
expect(env.XCODEBUILDMCP_SIMULATOR_ID).toBe('SIM-1');
1302+
});
10571303
});

0 commit comments

Comments
 (0)