Skip to content

Latest commit

 

History

History
769 lines (582 loc) · 25.8 KB

File metadata and controls

769 lines (582 loc) · 25.8 KB

XcodeBuildMCP Plugin Development Guide

This guide provides comprehensive instructions for creating new tools and workflow groups in XcodeBuildMCP using the filesystem-based auto-discovery system.

Table of Contents

  1. Overview
  2. Plugin Architecture
  3. Creating New Tools
  4. Creating New Workflow Groups
  5. Creating MCP Resources
  6. Auto-Discovery System
  7. Testing Guidelines
  8. Development Workflow
  9. Best Practices

Overview

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.

Key Features

  • 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

Plugin Architecture

Directory Structure

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

Plugin Tool Types

  1. Canonical Workflows: Standalone workflow groups (e.g., swift-package, ui-testing) defined as folders in the src/mcp/tools/ directory
  2. Shared Tools: Common tools in *-shared directories (not exposed to clients)
  3. Re-exported Tools: Share tools to other workflow groups by re-exporting them

Creating New Tools

1. Tool File Structure

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());
  },
};

2. Required Tool Plugin Properties

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>

3. Naming Conventions

Tools follow the pattern: {action}_{target}_{specifier}_{projectType}

Examples:

  • build_sim_id_ws → Build + Simulator + ID + Workspace
  • build_sim_name_proj → Build + Simulator + Name + Project
  • test_device_ws → Test + Device + Workspace
  • swift_package_build → Swift Package + Build

Project Type Suffixes:

  • _ws → Works with .xcworkspace files
  • _proj → Works with .xcodeproj files
  • No suffix → Generic or canonical tools

4. Parameter Validation Patterns

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;

5. Response Patterns

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
};

Creating New Workflow Groups

1. Workflow Group Structure

Each workflow group requires:

  1. Directory: Following naming convention
  2. Workflow Metadata: index.ts file with workflow export
  3. Tool Files: Individual tool implementations
  4. Tests: Comprehensive test coverage

2. Directory Naming Convention

[platform]-[projectType]/     # e.g., simulator-workspace, device-project
[platform]-shared/            # e.g., simulator-shared, macos-shared
[workflow-name]/               # e.g., swift-package, ui-testing

3. Workflow Metadata (index.ts)

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 name
  • description: Clear description of workflow purpose

4. Tool Organization Patterns

Canonical Workflow Groups

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
    └── ...

Shared Workflow Groups

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
    └── ...

Project/Workspace Workflow Groups

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
    └── ...

5. Re-export Implementation

For project/workspace groups that share tools:

// simulator-project/boot_sim.ts
export { default } from '../simulator-shared/boot_sim.js';

Re-export Rules:

  1. Re-exports come from canonical -shared groups
  2. No chained re-exports (re-exports from re-exports)
  3. Each tool maintains project or workspace specificity
  4. Implementation shared, interfaces remain unique

Creating MCP Resources

MCP Resources provide efficient URI-based data access for clients that support the MCP resource specification

1. Resource Structure

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());
  },
};

2. Resource Implementation Guidelines

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.

3. Resource Testing

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');
    });
  });
});

4. Auto-Discovery

Resources are automatically discovered and loaded by the build system. After creating a resource:

  1. Run npm run build to regenerate resource loaders
  2. The resource will be available at its URI for supported clients

Auto-Discovery System

How Auto-Discovery Works

  1. Filesystem Scan: loadPlugins() scans src/mcp/tools/ directory
  2. Workflow Loading: Each subdirectory is treated as a potential workflow group
  3. Metadata Validation: index.ts files provide workflow metadata
  4. Tool Discovery: All .ts files (except tests and index) are loaded as tools
  5. Registration: Tools are automatically registered with the MCP server

Discovery Process

// Simplified discovery flow
const plugins = await loadPlugins();
for (const plugin of plugins.values()) {
  server.tool(plugin.name, plugin.description, plugin.schema, plugin.handler);
}

Selective Workflow Loading

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-discovery

XCODEBUILDMCP_DEBUG=true can still be used to increase logging verbosity.

Testing Guidelines

Test Organization

__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

Dependency Injection Testing

✅ 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();

Three-Dimensional Testing

Every tool test must cover:

  1. Input Validation: Parameter schema validation and error cases
  2. Command Generation: Verify correct CLI commands are built
  3. Output Processing: Test response formatting and error handling

Test Template

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');
    });
  });
});

Development Workflow

Adding a New Tool

  1. Choose Directory: Select appropriate workflow group or create new one
  2. Create Tool File: Follow naming convention and structure
  3. Implement Logic: Use dependency injection pattern
  4. Define Schema: Add comprehensive Zod validation
  5. Write Tests: Cover all three dimensions
  6. Test Integration: Build and verify auto-discovery

Step-by-Step Tool Creation

# 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

Adding a New Workflow Group

  1. Create Directory: Follow naming convention
  2. Add Workflow Metadata: Create index.ts with workflow export
  3. Implement Tools: Add tool files following patterns
  4. Create Tests: Add comprehensive test coverage
  5. Verify Discovery: Test auto-discovery and tool registration

Step-by-Step Workflow Creation

# 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

Best Practices

Tool Design

  1. Single Responsibility: Each tool should have one clear purpose
  2. Descriptive Names: Follow naming conventions for discoverability
  3. Clear Descriptions: Include usage examples in tool descriptions
  4. Comprehensive Validation: Validate all parameters with helpful error messages
  5. Consistent Responses: Use utility functions for response formatting

Error Handling

  1. Graceful Failures: Always return ToolResponse, never throw from handlers
  2. Descriptive Errors: Provide actionable error messages
  3. Error Types: Use appropriate error types for different scenarios
  4. Logging: Log important events and errors for debugging

Testing

  1. Dependency Injection: Always test with mock executors
  2. Complete Coverage: Test all input, command, and output scenarios
  3. Literal Assertions: Use exact string expectations to catch changes
  4. Fast Execution: Tests should complete quickly without real system calls

Workflow Organization

  1. End-to-End Workflows: Groups should provide complete functionality
  2. Logical Grouping: Group related tools together
  3. Clear Capabilities: Document what each workflow can accomplish
  4. Consistent Patterns: Follow established patterns for maintainability

Workflow Metadata Considerations

  1. Workflow Completeness: Each group should be self-sufficient
  2. Clear Descriptions: Keep the description concise and user-focused

Updating TOOLS.md Documentation

Critical Documentation Maintenance

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.

Documentation Update Process

1. Use Tree CLI for Accurate Discovery

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.ts files)
  • Provides the actual proof of what exists in the codebase
  • Gives an accurate count of tools per workflow group

2. Ignore Shared Groups in Documentation

When updating TOOLS.md:

  • Ignore *-shared directories (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

3. List Actual Tool Names

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`

4. Systematic Documentation Update Steps

  1. Run the tree command to get current filesystem state
  2. Identify all non-shared workflow directories
  3. Count actual tool files in each directory (exclude index.ts and test files)
  4. List all tool names explicitly in the documentation
  5. Update tool counts to reflect actual numbers
  6. Verify consistency between filesystem and documentation

5. Documentation Formatting Requirements

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 description field 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

6. Validation Checklist

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

Why This Process Matters

  1. Accuracy: Tree command provides definitive proof of current state
  2. Maintainability: Systematic process prevents documentation drift
  3. User Experience: Accurate documentation helps users understand available tools
  4. 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.