This guide provides comprehensive instructions for creating new tools and workflow groups in XcodeBuildMCP using the filesystem-based auto-discovery system.
- Overview
- Plugin Architecture
- Creating New Tools
- Creating New Workflow Groups
- Creating MCP Resources
- Auto-Discovery System
- Testing Guidelines
- Development Workflow
- Best Practices
XcodeBuildMCP uses a plugin-based architecture with filesystem-based auto-discovery. Tools are automatically discovered and loaded without manual registration, and can be selectively enabled using XCODEBUILDMCP_ENABLED_WORKFLOWS.
- Auto-Discovery: Tools are automatically found by scanning
src/mcp/tools/directory - Selective Workflow Loading: Limit startup tool registration with
XCODEBUILDMCP_ENABLED_WORKFLOWS - Dependency Injection: All tools use testable patterns with mock-friendly executors
- Workflow Organization: Tools are grouped into end-to-end development workflows
src/mcp/tools/
├── simulator-workspace/ # iOS Simulator + Workspace tools
├── simulator-project/ # iOS Simulator + Project tools (re-exports)
├── simulator-shared/ # Shared simulator tools (canonical)
├── device-workspace/ # iOS Device + Workspace tools
├── device-project/ # iOS Device + Project tools (re-exports)
├── device-shared/ # Shared device tools (canonical)
├── macos-workspace/ # macOS + Workspace tools
├── macos-project/ # macOS + Project tools (re-exports)
├── macos-shared/ # Shared macOS tools (canonical)
├── swift-package/ # Swift Package Manager tools
├── ui-testing/ # UI automation tools
├── project-discovery/ # Project analysis tools
├── utilities/ # General utilities
├── doctor/ # System health check tools
└── logging/ # Log capture tools
- Canonical Workflows: Standalone workflow groups (e.g.,
swift-package,ui-testing) defined as folders in thesrc/mcp/tools/directory - Shared Tools: Common tools in
*-shareddirectories (not exposed to clients) - Re-exported Tools: Share tools to other workflow groups by re-exporting them
Every tool follows this standardized pattern:
// src/mcp/tools/my-workflow/my_tool.ts
import { z } from 'zod';
import { ToolResponse } from '../../../types/common.js';
import { CommandExecutor, getDefaultCommandExecutor } from '../../../utils/command.js';
import { log, validateRequiredParam, createTextResponse, createErrorResponse } from '../../../utils/index.js';
// 1. Define parameters type for clarity
type MyToolParams = {
requiredParam: string;
optionalParam?: string;
};
// 2. Implement the core logic in a separate, testable function
export async function my_toolLogic(
params: MyToolParams,
executor: CommandExecutor,
): Promise<ToolResponse> {
// 3. Validate required parameters
const requiredValidation = validateRequiredParam('requiredParam', params.requiredParam);
if (!requiredValidation.isValid) {
return requiredValidation.errorResponse;
}
log('info', `Executing my_tool with param: ${params.requiredParam}`);
try {
// 4. Build and execute the command using the injected executor
const command = ['my-command', '--param', params.requiredParam];
if (params.optionalParam) {
command.push('--optional', params.optionalParam);
}
const result = await executor(command, 'My Tool Operation');
if (!result.success) {
return createErrorResponse('My Tool operation failed', result.error);
}
return createTextResponse(`✅ Success: ${result.output}`);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `My Tool execution error: ${errorMessage}`);
return createErrorResponse('Tool execution failed', errorMessage);
}
}
// 5. Export the tool definition as the default export
export default {
name: 'my_tool',
description: 'A brief description of what my_tool does, with a usage example. e.g. my_tool({ requiredParam: "value" })',
schema: {
requiredParam: z.string().describe('Description of the required parameter.'),
optionalParam: z.string().optional().describe('Description of the optional parameter.'),
},
// The handler wraps the logic function with the default executor for production use
handler: async (args: Record<string, unknown>): Promise<ToolResponse> => {
return my_toolLogic(args as MyToolParams, getDefaultCommandExecutor());
},
};Every tool plugin must export a default object with these properties:
| Property | Type | Description |
|---|---|---|
name |
string |
Tool name (must match filename without extension) |
description |
string |
Clear description with usage examples |
schema |
Record<string, z.ZodTypeAny> |
Zod validation schema for parameters |
handler |
function |
Async function: (args) => Promise<ToolResponse> |
Tools follow the pattern: {action}_{target}_{specifier}_{projectType}
Examples:
build_sim_id_ws→ Build + Simulator + ID + Workspacebuild_sim_name_proj→ Build + Simulator + Name + Projecttest_device_ws→ Test + Device + Workspaceswift_package_build→ Swift Package + Build
Project Type Suffixes:
_ws→ Works with.xcworkspacefiles_proj→ Works with.xcodeprojfiles- No suffix → Generic or canonical tools
Use utility functions for consistent validation:
// Required parameter validation
const pathValidation = validateRequiredParam('workspacePath', params.workspacePath);
if (!pathValidation.isValid) return pathValidation.errorResponse;
// At-least-one parameter validation
const identifierValidation = validateAtLeastOneParam(
'simulatorId', params.simulatorId,
'simulatorName', params.simulatorName
);
if (!identifierValidation.isValid) return identifierValidation.errorResponse;
// File existence validation
const fileValidation = validateFileExists(params.workspacePath as string);
if (!fileValidation.isValid) return fileValidation.errorResponse;Use utility functions for consistent responses:
// Success responses
return createTextResponse('✅ Operation succeeded');
return createTextResponse('Operation completed', false); // Not an error
// Error responses
return createErrorResponse('Operation failed', errorDetails);
return createErrorResponse('Validation failed', errorMessage, 'ValidationError');
// Complex responses
return {
content: [
{ type: 'text', text: '✅ Build succeeded' },
{ type: 'text', text: 'Next steps: Run install_app_sim...' }
],
isError: false
};Each workflow group requires:
- Directory: Following naming convention
- Workflow Metadata:
index.tsfile with workflow export - Tool Files: Individual tool implementations
- Tests: Comprehensive test coverage
[platform]-[projectType]/ # e.g., simulator-workspace, device-project
[platform]-shared/ # e.g., simulator-shared, macos-shared
[workflow-name]/ # e.g., swift-package, ui-testing
Required for all workflow groups:
// Example: src/mcp/tools/simulator-workspace/index.ts
export const workflow = {
name: 'iOS Simulator Workspace Development',
description: 'Complete iOS development workflow for .xcworkspace files including build, test, deploy, and debug capabilities',
};Required Properties:
name: Human-readable workflow namedescription: Clear description of workflow purpose
Self-contained workflows that don't re-export from other groups:
swift-package/
├── index.ts # Workflow metadata
├── swift_package_build.ts # Build tool
├── swift_package_test.ts # Test tool
├── swift_package_run.ts # Run tool
└── __tests__/ # Test directory
├── index.test.ts # Workflow tests
├── swift_package_build.test.ts
└── ...
Provide canonical tools for re-export by project/workspace variants:
simulator-shared/
├── boot_sim.ts # Canonical simulator boot tool
├── install_app_sim.ts # Canonical app install tool
└── __tests__/ # Test directory
├── boot_sim.test.ts
└── ...
Re-export shared tools and add variant-specific tools:
simulator-project/
├── index.ts # Workflow metadata
├── boot_sim.ts # Re-export: export { default } from '../simulator-shared/boot_sim.js';
├── build_sim_id_proj.ts # Project-specific build tool
└── __tests__/ # Test directory
├── index.test.ts # Workflow tests
├── re-exports.test.ts # Re-export validation
└── ...
For project/workspace groups that share tools:
// simulator-project/boot_sim.ts
export { default } from '../simulator-shared/boot_sim.js';Re-export Rules:
- Re-exports come from canonical
-sharedgroups - No chained re-exports (re-exports from re-exports)
- Each tool maintains project or workspace specificity
- Implementation shared, interfaces remain unique
MCP Resources provide efficient URI-based data access for clients that support the MCP resource specification
Resources are located in src/resources/ and follow this pattern:
// src/resources/example.ts
import { log, getDefaultCommandExecutor, CommandExecutor } from '../../utils/index.js';
// Testable resource logic separated from MCP handler
export async function exampleResourceLogic(
executor: CommandExecutor,
): Promise<{ contents: Array<{ text: string }> }> {
try {
log('info', 'Processing example resource request');
// Use the executor to get data
const result = await executor(['some', 'command'], 'Example Resource Operation');
if (!result.success) {
throw new Error(result.error || 'Failed to get resource data');
}
return {
contents: [{ text: result.output || 'resource data' }]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
log('error', `Error in example resource handler: ${errorMessage}`);
return {
contents: [
{
text: `Error retrieving resource data: ${errorMessage}`,
},
],
};
}
}
export default {
uri: 'xcodebuildmcp://example',
name: 'example',
description: 'Description of the resource data',
mimeType: 'text/plain',
async handler(_uri: URL): Promise<{ contents: Array<{ text: string }> }> {
return exampleResourceLogic(getDefaultCommandExecutor());
},
};Reuse Existing Logic: Resources that mirror tools should reuse existing tool logic for consistency:
// src/mcp/resources/simulators.ts (simplified example)
import { list_simsLogic } from '../tools/simulator-shared/list_sims.js';
export default {
uri: 'xcodebuildmcp://simulators',
name: 'simulators'
description: 'Available iOS simulators with UUIDs and states',
mimeType: 'text/plain',
async handler(uri: URL): Promise<{ contents: Array<{ text: string }> }> {
const executor = getDefaultCommandExecutor();
const result = await list_simsLogic({}, executor);
return {
contents: [{ text: result.content[0].text }]
};
}
};As not all clients support resources it important that resource content that would be ideally be served by resources be mirroed as a tool as well. This ensurew clients that don't support this capability continue to will still have access to that resource data via a simple tool call.
Create tests in src/mcp/resources/__tests__/:
// src/mcp/resources/__tests__/example.test.ts
import exampleResource, { exampleResourceLogic } from '../example.js';
import { createMockExecutor } from '../../utils/test-common.js';
describe('example resource', () => {
describe('Export Field Validation', () => {
it('should export correct uri', () => {
expect(exampleResource.uri).toBe('xcodebuildmcp://example');
});
it('should export correct description', () => {
expect(exampleResource.description).toBe('Description of the resource data');
});
it('should export correct mimeType', () => {
expect(exampleResource.mimeType).toBe('text/plain');
});
it('should export handler function', () => {
expect(typeof exampleResource.handler).toBe('function');
});
});
describe('Resource Logic Functionality', () => {
it('should return resource data successfully', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'test data'
});
// Test the logic function directly, not the handler
const result = await exampleResourceLogic(mockExecutor);
expect(result.contents).toHaveLength(1);
expect(result.contents[0].text).toContain('expected data');
});
it('should handle command execution errors', async () => {
const mockExecutor = createMockExecutor({
success: false,
error: 'Command failed'
});
const result = await exampleResourceLogic(mockExecutor);
expect(result.contents[0].text).toContain('Error retrieving');
});
});
});Resources are automatically discovered and loaded by the build system. After creating a resource:
- Run
npm run buildto regenerate resource loaders - The resource will be available at its URI for supported clients
- Filesystem Scan:
loadPlugins()scanssrc/mcp/tools/directory - Workflow Loading: Each subdirectory is treated as a potential workflow group
- Metadata Validation:
index.tsfiles provide workflow metadata - Tool Discovery: All
.tsfiles (except tests and index) are loaded as tools - Registration: Tools are automatically registered with the MCP server
// Simplified discovery flow
const plugins = await loadPlugins();
for (const plugin of plugins.values()) {
server.tool(plugin.name, plugin.description, plugin.schema, plugin.handler);
}To limit which workflows are registered at startup, set XCODEBUILDMCP_ENABLED_WORKFLOWS to a comma-separated list of workflow directory names. The session-management workflow is always auto-included since other tools depend on it.
Example:
XCODEBUILDMCP_ENABLED_WORKFLOWS=simulator,device,project-discoveryXCODEBUILDMCP_DEBUG=true can still be used to increase logging verbosity.
__tests__/
├── index.test.ts # Workflow metadata tests (canonical groups only)
├── re-exports.test.ts # Re-export validation (project/workspace groups)
└── tool_name.test.ts # Individual tool tests
✅ CORRECT Pattern:
import { createMockExecutor } from '../../../utils/test-common.js';
describe('build_sim_name_ws', () => {
it('should build successfully', async () => {
const mockExecutor = createMockExecutor({
success: true,
output: 'BUILD SUCCEEDED'
});
const result = await build_sim_name_wsLogic(params, mockExecutor);
expect(result.isError).toBe(false);
});
});❌ FORBIDDEN Pattern (Vitest Mocking Banned):
// ❌ ALL VITEST MOCKING IS COMPLETELY BANNED
vi.mock('child_process');
const mockSpawn = vi.fn();Every tool test must cover:
- Input Validation: Parameter schema validation and error cases
- Command Generation: Verify correct CLI commands are built
- Output Processing: Test response formatting and error handling
import { describe, it, expect } from 'vitest';
import { createMockExecutor } from '../../../utils/test-common.js';
import tool, { toolNameLogic } from '../tool_name.js';
describe('tool_name', () => {
describe('Export Validation', () => {
it('should export correct name', () => {
expect(tool.name).toBe('tool_name');
});
it('should export correct description', () => {
expect(tool.description).toContain('Expected description');
});
it('should export handler function', () => {
expect(typeof tool.handler).toBe('function');
});
});
describe('Parameter Validation', () => {
it('should validate required parameters', async () => {
const mockExecutor = createMockExecutor({ success: true, output: '' });
const result = await toolNameLogic({}, mockExecutor);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain("Required parameter");
});
});
describe('Command Generation', () => {
it('should generate correct command', async () => {
const mockExecutor = createMockExecutor({ success: true, output: 'SUCCESS' });
await toolNameLogic({ param: 'value' }, mockExecutor);
expect(mockExecutor).toHaveBeenCalledWith(
expect.arrayContaining(['expected', 'command']),
expect.any(String),
expect.any(Boolean)
);
});
});
describe('Response Processing', () => {
it('should handle successful execution', async () => {
const mockExecutor = createMockExecutor({ success: true, output: 'SUCCESS' });
const result = await toolNameLogic({ param: 'value' }, mockExecutor);
expect(result.isError).toBe(false);
expect(result.content[0].text).toContain('✅');
});
it('should handle execution errors', async () => {
const mockExecutor = createMockExecutor({ success: false, error: 'Command failed' });
const result = await toolNameLogic({ param: 'value' }, mockExecutor);
expect(result.isError).toBe(true);
expect(result.content[0].text).toContain('Command failed');
});
});
});- Choose Directory: Select appropriate workflow group or create new one
- Create Tool File: Follow naming convention and structure
- Implement Logic: Use dependency injection pattern
- Define Schema: Add comprehensive Zod validation
- Write Tests: Cover all three dimensions
- Test Integration: Build and verify auto-discovery
# 1. Create tool file
touch src/mcp/tools/simulator-workspace/my_new_tool_ws.ts
# 2. Implement tool following patterns above
# 3. Create test file
touch src/mcp/tools/simulator-workspace/__tests__/my_new_tool_ws.test.ts
# 4. Build project
npm run build
# 5. Verify tool is discovered (should appear in tools list)
npm run inspect # Use MCP Inspector to verify- Create Directory: Follow naming convention
- Add Workflow Metadata: Create
index.tswith workflow export - Implement Tools: Add tool files following patterns
- Create Tests: Add comprehensive test coverage
- Verify Discovery: Test auto-discovery and tool registration
# 1. Create workflow directory
mkdir src/mcp/tools/my-new-workflow
# 2. Create workflow metadata
cat > src/mcp/tools/my-new-workflow/index.ts << 'EOF'
export const workflow = {
name: 'My New Workflow',
description: 'Description of workflow capabilities',
};
EOF
# 3. Create tools directory and test directory
mkdir src/mcp/tools/my-new-workflow/__tests__
# 4. Implement tools following patterns
# 5. Build and verify
npm run build
npm run inspect- Single Responsibility: Each tool should have one clear purpose
- Descriptive Names: Follow naming conventions for discoverability
- Clear Descriptions: Include usage examples in tool descriptions
- Comprehensive Validation: Validate all parameters with helpful error messages
- Consistent Responses: Use utility functions for response formatting
- Graceful Failures: Always return ToolResponse, never throw from handlers
- Descriptive Errors: Provide actionable error messages
- Error Types: Use appropriate error types for different scenarios
- Logging: Log important events and errors for debugging
- Dependency Injection: Always test with mock executors
- Complete Coverage: Test all input, command, and output scenarios
- Literal Assertions: Use exact string expectations to catch changes
- Fast Execution: Tests should complete quickly without real system calls
- End-to-End Workflows: Groups should provide complete functionality
- Logical Grouping: Group related tools together
- Clear Capabilities: Document what each workflow can accomplish
- Consistent Patterns: Follow established patterns for maintainability
- Workflow Completeness: Each group should be self-sufficient
- Clear Descriptions: Keep the
descriptionconcise and user-focused
Every time you add, change, move, edit, or delete a tool, you MUST review and update the TOOLS.md file to reflect the current state of the codebase.
Always use the tree command to get the actual filesystem representation of tools:
# Get the definitive source of truth for all workflow groups and tools
tree src/mcp/tools/ -I "__tests__" -I "*.test.ts"This command:
- Shows ALL workflow directories and their tools
- Excludes test files (
__tests__directories and*.test.tsfiles) - Provides the actual proof of what exists in the codebase
- Gives an accurate count of tools per workflow group
When updating TOOLS.md:
- Ignore
*-shareddirectories (e.g.,simulator-shared,device-shared,macos-shared) - These are implementation details, not user-facing workflow groups
- Only document the main workflow groups that users interact with
- The group count should exclude shared groups
Instead of using generic descriptions like "Additional Tools: Simulator management, logging, UI testing tools":
❌ Wrong:
- **Additional Tools**: Simulator management, logging, UI testing tools✅ Correct:
- `boot_sim`, `install_app_sim`, `launch_app_sim`, `list_sims`, `open_sim`
- `describe_ui`, `screenshot`, `start_sim_log_cap`, `stop_sim_log_cap`- Run the tree command to get current filesystem state
- Identify all non-shared workflow directories
- Count actual tool files in each directory (exclude
index.tsand test files) - List all tool names explicitly in the documentation
- Update tool counts to reflect actual numbers
- Verify consistency between filesystem and documentation
Format: One Tool Per Bullet Point with Description
Each tool must be listed individually with its actual description from the tool file:
### 1. My Awesome Workflow (`my-awesome-workflow`)
**Purpose**: A short description of what this workflow is for. (2 tools)
- `my_tool_one` - Description for my_tool_one from its definition file.
- `my_tool_two` - Description for my_tool_two from its definition file.Description Sources:
- Use the actual
descriptionfield from each tool's TypeScript file - Descriptions should be concise but informative for end users
- Include platform/context information (iOS, macOS, simulator, device, etc.)
- Mention required parameters when critical for usage
After updating TOOLS.md:
- Tool counts match actual filesystem counts (from tree command)
- Each tool has its own bullet point (one tool per line)
- Each tool includes its actual description from the tool file
- No generic descriptions like "Additional Tools: X, Y, Z"
- Descriptions are user-friendly and informative
- Shared groups (
*-shared) are not included in main workflow list - Workflow group count reflects only user-facing groups (15 groups)
- Tree command output was used as source of truth
- Documentation is user-focused, not implementation-focused
- Tool names are in alphabetical order within each workflow group
- Accuracy: Tree command provides definitive proof of current state
- Maintainability: Systematic process prevents documentation drift
- User Experience: Accurate documentation helps users understand available tools
- Development Confidence: Developers can trust the documentation reflects reality
Remember: The filesystem is the source of truth. Documentation must always reflect the actual codebase structure, and the tree command is the most reliable way to ensure accuracy.