Skip to content

Commit 55ad4e6

Browse files
committed
refactor(architecture): separate server, test, and pure utils to fix CI errors
1 parent 29fb327 commit 55ad4e6

37 files changed

Lines changed: 1140 additions & 873 deletions

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ This project uses [Sentry](https://sentry.io/) for error monitoring and diagnost
362362
- Error logs may include details such as error messages, stack traces, and (in some cases) file paths or project names. You can review the sources in this repository to see exactly what is logged.
363363

364364
### Opting Out of Sentry
365-
- If you do not wish to send error logs to Sentry, you can opt out by setting the environment variable `SENTRY_DISABLED=true`.
365+
- If you do not wish to send error logs to Sentry, you can opt out by setting the environment variable `XCODEBUILDMCP_SENTRY_DISABLED=true`.
366366

367367
Example MCP client configuration:
368368
```json
@@ -375,7 +375,7 @@ Example MCP client configuration:
375375
"xcodebuildmcp@latest"
376376
],
377377
"env": {
378-
"SENTRY_DISABLED": "true"
378+
"XCODEBUILDMCP_SENTRY_DISABLED": "true"
379379
}
380380
}
381381
}

docs/TOOLS.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# XcodeBuildMCP Tools Reference
22

3-
XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehensive Apple development workflows.
3+
XcodeBuildMCP provides 59 tools organized into 12 workflow groups for comprehensive Apple development workflows.
44

55
## Key Changes (v1.11+)
66

77
**Unified Tool Architecture**: Tools that previously had separate variants (e.g., `build_sim_id`, `build_sim_name`) have been consolidated into unified tools that accept either parameter using XOR validation.
88

9-
**XOR Parameter Pattern**: Many tools now use mutually exclusive parameters (e.g., `simulatorId` OR `simulatorName`, never both) enforced via Zod schema refinements. This reduces the total tool count from ~85 to 61 while maintaining full functionality.
9+
**XOR Parameter Pattern**: Many tools now use mutually exclusive parameters (e.g., `simulatorId` OR `simulatorName`, never both) enforced via Zod schema refinements. This reduces the total tool count from ~85 to 59 while maintaining full functionality.
1010

1111
## Workflow Groups
1212

@@ -27,21 +27,19 @@ XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehens
2727
- `test_device` - Runs tests for an Apple project or workspace on a physical device (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) using xcodebuild test and parses xcresult output. Provide exactly one of projectPath or workspacePath.
2828

2929
### iOS Simulator Development (`simulator`)
30-
**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators. (13 tools)
30+
**Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators. (11 tools)
3131

3232
- `boot_sim` - Boots an iOS simulator. After booting, use open_sim() to make the simulator visible.
33-
- `build_run_simulator` - Builds and runs an app from a project or workspace on a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
34-
- `build_simulator` - Builds an app from a project or workspace for a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
35-
- `get_simulator_app_path` - Gets the app bundle path for a simulator by UUID or name using either a project or workspace file.
33+
- `build_run_sim` - Builds and runs an app from a project or workspace on a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
34+
- `build_sim` - Builds an app from a project or workspace for a specific simulator by UUID or name. Provide exactly one of projectPath or workspacePath, and exactly one of simulatorId or simulatorName.
35+
- `get_sim_app_path` - Gets the app bundle path for a simulator by UUID or name using either a project or workspace file.
3636
- `install_app_sim` - Installs an app in an iOS simulator.
3737
- `launch_app_logs_sim` - Launches an app in an iOS simulator and captures its logs.
38-
- `launch_app_sim` - Launches an app in an iOS simulator. If simulator window isn't visible, use open_sim() first. IMPORTANT: You MUST provide both the simulatorUuid and bundleId parameters. Note: You must install the app in the simulator before launching. The typical workflow is: build → install → launch. Example: launch_app_sim({ simulatorUuid: 'YOUR_UUID_HERE', bundleId: 'com.example.MyApp' })
39-
- `launch_app_sim_name` - Launches an app in an iOS simulator by simulator name. If simulator window isn't visible, use open_sim() first. IMPORTANT: You MUST provide both the simulatorName and bundleId parameters. Note: You must install the app in the simulator before launching. The typical workflow is: build → install → launch. Example: launch_app_sim_name({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
38+
- `launch_app_sim` - Launches an app in an iOS simulator by UUID or name. If simulator window isn't visible, use open_sim() first. or launch_app_sim({ simulatorName: 'iPhone 16', bundleId: 'com.example.MyApp' })
4039
- `list_sims` - Lists available iOS simulators with their UUIDs.
4140
- `open_sim` - Opens the iOS Simulator app.
42-
- `stop_app_sim` - Stops an app running in an iOS simulator. Requires simulatorUuid and bundleId.
43-
- `stop_app_sim_name` - Stops an app running in an iOS simulator by simulator name. IMPORTANT: You MUST provide both the simulatorName and bundleId parameters.
44-
- `test_simulator` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
41+
- `stop_app_sim` - Stops an app running in an iOS simulator by UUID or name. or stop_app_sim({ simulatorName: "iPhone 16", bundleId: "com.example.MyApp" })
42+
- `test_sim` - Runs tests on a simulator by UUID or name using xcodebuild test and parses xcresult output. Works with both Xcode projects (.xcodeproj) and workspaces (.xcworkspace).
4543

4644
### Log Capture & Management (`logging`)
4745
**Purpose**: Log capture and management tools for iOS simulators and physical devices. Start, stop, and analyze application and system logs during development and testing. (4 tools)
@@ -56,7 +54,7 @@ XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehens
5654

5755
- `build_macos` - Builds a macOS app using xcodebuild from a project or workspace. Provide exactly one of projectPath or workspacePath. Example: build_macos({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })
5856
- `build_run_macos` - Builds and runs a macOS app from a project or workspace in one step. Provide exactly one of projectPath or workspacePath. Example: build_run_macos({ projectPath: '/path/to/MyProject.xcodeproj', scheme: 'MyScheme' })
59-
- `get_macos_app_path` - Gets the app bundle path for a macOS application using either a project or workspace. Provide exactly one of projectPath or workspacePath. Example: get_macos_app_path({ projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme' })
57+
- `get_mac_app_path` - Gets the app bundle path for a macOS application using either a project or workspace. Provide exactly one of projectPath or workspacePath. Example: get_mac_app_path({ projectPath: '/path/to/project.xcodeproj', scheme: 'MyScheme' })
6058
- `launch_mac_app` - Launches a macOS application. Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.
6159
- `stop_mac_app` - Stops a running macOS application. Can stop by app name or process ID.
6260
- `test_macos` - Runs tests for a macOS project or workspace using xcodebuild test and parses xcresult output. Provide exactly one of projectPath or workspacePath.
@@ -84,9 +82,9 @@ XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehens
8482
### Simulator Management (`simulator-management`)
8583
**Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators and setting simulator environment options like location, network, statusbar and appearance. (4 tools)
8684

87-
- `reset_simulator_location` - Resets the simulator's location to default.
85+
- `reset_sim_location` - Resets the simulator's location to default.
8886
- `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator.
89-
- `set_simulator_location` - Sets a custom GPS location for the simulator.
87+
- `set_sim_location` - Sets a custom GPS location for the simulator.
9088
- `sim_statusbar` - Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).
9189

9290
### Swift Package Manager (`swift-package`)
@@ -123,10 +121,10 @@ XcodeBuildMCP provides 61 tools organized into 12 workflow groups for comprehens
123121

124122
## Summary Statistics
125123

126-
- **Total Tools**: 61 canonical tools + 22 re-exports = 83 total
124+
- **Total Tools**: 59 canonical tools + 22 re-exports = 81 total
127125
- **Workflow Groups**: 12
128126
- **Analysis Method**: Static AST parsing with TypeScript compiler API
129127

130128
---
131129

132-
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-08-13*
130+
*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2025-08-15*

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default [
66
eslint.configs.recommended,
77
...tseslint.configs.recommended,
88
{
9-
ignores: ['node_modules/**', 'build/**', 'dist/**', 'coverage/**', 'src/core/generated-plugins.ts', 'src/core/generated-resources.ts'],
9+
ignores: ['node_modules/**', 'build/**', 'dist/**', 'coverage/**', 'src/**/generated-*.ts'],
1010
},
1111
{
1212
// TypeScript files in src/ directory (covered by tsconfig.json)

src/core/plugin-registry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { PluginMeta, WorkflowGroup, WorkflowMeta } from './plugin-types.js';
1+
import type { PluginMeta, WorkflowGroup, WorkflowMeta } from '../server/plugin-types.js';
22
import { WORKFLOW_LOADERS, WorkflowName, WORKFLOW_METADATA } from './generated-plugins.js';
33

44
export async function loadPlugins(): Promise<Map<string, PluginMeta>> {

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
registerDiscoveryTools,
3939
registerAllToolsStatic,
4040
registerSelectedWorkflows,
41-
} from './utils/tool-registry.js';
41+
} from './server/tool-registry.js';
4242

4343
/**
4444
* Main function to start the server
@@ -62,7 +62,7 @@ async function main(): Promise<void> {
6262
}
6363

6464
// Create the server
65-
const server = createServer();
65+
const server = await createServer();
6666

6767
// Make server available globally for dynamic tools
6868
(globalThis as { mcpServer?: McpServer }).mcpServer = server;

src/mcp/tools/discovery/discover_tools.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
enableWorkflows,
88
getAvailableWorkflows,
99
generateWorkflowDescriptions,
10-
} from '../../../core/dynamic-tools.js';
10+
} from '../../../server/dynamic-tools.js';
1111
import { createTypedTool } from '../../../utils/typed-tool-factory.js';
1212
import { getDefaultCommandExecutor } from '../../../utils/command.js';
1313
import { McpServer } from '@camsoft/mcp-sdk/server/mcp.js';

src/mcp/tools/doctor/__tests__/doctor.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function createDeps(overrides?: Partial<DoctorDependencies>): DoctorDependencies
3737
USER: 'testuser',
3838
TMPDIR: '/tmp',
3939
NODE_ENV: 'test',
40-
SENTRY_DISABLED: 'false',
40+
XCODEBUILDMCP_SENTRY_DISABLED: 'false',
4141
};
4242
return x;
4343
},
@@ -260,7 +260,7 @@ describe('doctor tool', () => {
260260
USER: 'testuser',
261261
TMPDIR: '/tmp',
262262
NODE_ENV: 'test',
263-
SENTRY_DISABLED: 'true',
263+
XCODEBUILDMCP_SENTRY_DISABLED: 'true',
264264
};
265265
return x;
266266
},

src/mcp/tools/doctor/doctor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export async function runDoctor(
219219
`- Incremental Build Support: ${doctorInfo.features.xcodemake.available && doctorInfo.features.xcodemake.enabled ? '\u2705 Available & Enabled' : doctorInfo.features.xcodemake.available ? '\u2705 Available but Disabled' : '\u274c Not available'}`,
220220

221221
`\n## Sentry`,
222-
`- Sentry enabled: ${doctorInfo.environmentVariables.SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`,
222+
`- Sentry enabled: ${doctorInfo.environmentVariables.XCODEBUILDMCP_SENTRY_DISABLED !== 'true' ? '✅ Yes' : '❌ No'}`,
223223

224224
`\n## Troubleshooting Tips`,
225225
`- If UI automation tools are not available, install axe: \`brew tap cameroncooke/axe && brew install axe\``,

src/mcp/tools/doctor/lib/doctor.deps.ts

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import * as os from 'os';
22
import {
33
CommandExecutor,
4-
loadWorkflowGroups,
5-
loadPlugins,
64
areAxeToolsAvailable,
75
isXcodemakeEnabled,
86
isXcodemakeAvailable,
97
doesMakefileExist,
10-
getEnabledWorkflows,
118
} from '../../../../utils/index.js';
12-
import { getTrackedToolNames } from '../../../../utils/tool-registry.js';
9+
import {
10+
getXcodeInfo,
11+
getEnvironmentVariables,
12+
checkBinaryAvailability,
13+
} from '../../../../utils/system-info.js';
14+
import { loadWorkflowGroups, loadPlugins } from '../../../../core/plugin-registry.js';
15+
import { getEnabledWorkflows } from '../../../../server/dynamic-tools.js';
16+
import { getTrackedToolNames } from '../../../../server/tool-registry.js';
1317

1418
export interface BinaryChecker {
1519
checkBinaryAvailability(binary: string): Promise<{ available: boolean; version?: string }>;
@@ -99,56 +103,32 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen
99103
if (binary === 'axe' && areAxeToolsAvailable()) {
100104
return { available: true, version: 'Bundled' };
101105
}
102-
try {
103-
const which = await executor(['which', binary], 'Check Binary Availability');
104-
if (!which.success) {
105-
return { available: false };
106-
}
107-
} catch {
108-
return { available: false };
109-
}
110-
111-
let version: string | undefined;
112-
const versionCommands: Record<string, string> = {
113-
axe: 'axe --version',
114-
mise: 'mise --version',
115-
};
116-
117-
if (binary in versionCommands) {
118-
try {
119-
const res = await executor(versionCommands[binary]!.split(' '), 'Get Binary Version');
120-
if (res.success && res.output) {
121-
version = res.output.trim();
122-
}
123-
} catch {
124-
// ignore
125-
}
126-
}
127106

128-
return { available: true, version: version ?? 'Available (version info not available)' };
107+
// Use shared system info utility for consistency
108+
return checkBinaryAvailability(binary);
129109
},
130110
};
131111

132112
const xcode: XcodeInfoProvider = {
133113
async getXcodeInfo() {
134114
try {
135-
const xcodebuild = await executor(['xcodebuild', '-version'], 'Get Xcode Version');
136-
if (!xcodebuild.success) throw new Error('xcodebuild command failed');
137-
const version = xcodebuild.output.trim().split('\n').slice(0, 2).join(' - ');
138-
139-
const pathRes = await executor(['xcode-select', '-p'], 'Get Xcode Path');
140-
if (!pathRes.success) throw new Error('xcode-select command failed');
141-
const path = pathRes.output.trim();
142-
143-
const selected = await executor(['xcrun', '--find', 'xcodebuild'], 'Find Xcodebuild');
144-
if (!selected.success) throw new Error('xcrun --find command failed');
145-
const selectedXcode = selected.output.trim();
115+
// Use shared system info utility for basic info
116+
const basicInfo = getXcodeInfo();
117+
if (basicInfo.error) {
118+
return { error: basicInfo.error };
119+
}
146120

121+
// Get additional xcrun version info using executor
147122
const xcrun = await executor(['xcrun', '--version'], 'Get Xcrun Version');
148123
if (!xcrun.success) throw new Error('xcrun --version command failed');
149124
const xcrunVersion = xcrun.output.trim();
150125

151-
return { version, path, selectedXcode, xcrunVersion };
126+
return {
127+
version: basicInfo.version,
128+
path: basicInfo.path,
129+
selectedXcode: basicInfo.selectedXcode,
130+
xcrunVersion,
131+
};
152132
} catch (error) {
153133
return { error: error instanceof Error ? error.message : String(error) };
154134
}
@@ -157,29 +137,16 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen
157137

158138
const env: EnvironmentInfoProvider = {
159139
getEnvironmentVariables() {
160-
const relevantVars = [
161-
'INCREMENTAL_BUILDS_ENABLED',
162-
'PATH',
163-
'DEVELOPER_DIR',
164-
'HOME',
165-
'USER',
166-
'TMPDIR',
167-
'NODE_ENV',
168-
'SENTRY_DISABLED',
169-
];
170-
171-
const envVars: Record<string, string | undefined> = {};
172-
for (const varName of relevantVars) {
173-
envVars[varName] = process.env[varName];
174-
}
140+
// Use shared system info utility for consistency
141+
const envVars = getEnvironmentVariables();
175142

176-
Object.keys(process.env).forEach((key) => {
177-
if (key.startsWith('XCODEBUILDMCP_')) {
178-
envVars[key] = process.env[key];
179-
}
143+
// Convert to match interface (string | undefined)
144+
const result: Record<string, string | undefined> = {};
145+
Object.entries(envVars).forEach(([key, value]) => {
146+
result[key] = value || undefined;
180147
});
181148

182-
return envVars;
149+
return result;
183150
},
184151

185152
getSystemInfo() {
@@ -218,7 +185,9 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen
218185
let totalPlugins = 0;
219186

220187
for (const [dirName, wf] of workflows.entries()) {
221-
const toolNames = wf.tools.map((t) => t.name).filter(Boolean) as string[];
188+
const toolNames = wf.tools
189+
.map((t: { name?: string }) => t.name)
190+
.filter(Boolean) as string[];
222191
totalPlugins += toolNames.length;
223192
pluginsByDirectory[dirName] = toolNames;
224193
}

src/mcp/tools/simulator/test_sim.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77
*/
88

99
import { z } from 'zod';
10-
import { handleTestLogic, log } from '../../../utils/index.js';
10+
import { handleTestLogic } from '../../../test-utils/test-execution.js';
11+
import { log } from '../../../utils/index.js';
1112
import { XcodePlatform } from '../../../utils/index.js';
1213
import { ToolResponse } from '../../../types/common.js';
1314
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';

0 commit comments

Comments
 (0)