Skip to content

Contributing

  1. Clone the repository:

    Terminal window
    git clone https://github.com/sitharaj88/xcode-pilot-mcp.git
    cd xcode-pilot-mcp
  2. Install dependencies:

    Terminal window
    npm install
  3. Run the quality gate:

    Terminal window
    npm run check

    This runs typecheck, lint, format check, and tests in sequence.

  4. Build:

    Terminal window
    npm run build
ScriptCommandDescription
npm run buildtsc && copy-templatesCompile TypeScript and copy templates
npm run devtsc --watchWatch mode for development
npm testvitest runRun all tests once
npm run test:coveragevitest run --coverageRun tests with coverage report
npm run linteslint .Check for lint issues
npm run lint:fixeslint . --fixAuto-fix lint issues
npm run formatprettier --write .Format all files
npm run format:checkprettier --check .Check formatting
npm run typechecktsc --noEmitType check without emitting
npm run checkAll of the aboveFull quality gate
  1. Create the handler file:

    src/tools/<category>/<tool-name>.ts
    import { executeCommand } from "../../executor.js";
    import { textResponse } from "../../utils/response.js";
    import { validateAbsolutePath } from "../../utils/validation.js";
    import type { Environment } from "../../types.js";
    interface MyToolParams {
    requiredParam: string;
    optionalParam?: string;
    }
    export async function myTool(
    params: MyToolParams,
    env: Environment,
    ) {
    validateAbsolutePath(params.requiredParam);
    const args = ["my-command", params.requiredParam];
    if (params.optionalParam) {
    args.push("--flag", params.optionalParam);
    }
    const result = await executeCommand(env.xcrunPath, args);
    if (!result.success) {
    return { content: [{ type: "text" as const, text: result.stderr }], isError: true };
    }
    return textResponse(result.stdout);
    }
  2. Register the tool in the category index:

    src/tools/<category>/index.ts
    import { z } from "zod";
    import { withErrorHandling } from "../../utils/response.js";
    import { myTool } from "./my-tool.js";
    // Add to the existing register function:
    server.tool(
    "my_tool",
    "Description of what the tool does",
    {
    requiredParam: z.string().describe("What this parameter is for"),
    optionalParam: z.string().optional().describe("Optional flag"),
    },
    withErrorHandling(async (params) => myTool(params, env)),
    );
  3. Write tests:

    tests/tools/<category>.test.ts
    import { describe, it, expect, beforeEach, vi } from "vitest";
    const { execFile } = vi.hoisted(() => ({
    execFile: vi.fn(),
    }));
    vi.mock("node:child_process", () => ({ execFile, spawn: vi.fn() }));
    import { myTool } from "../../src/tools/<category>/my-tool.js";
    const env = {
    xcodePath: "/Applications/Xcode.app/Contents/Developer",
    xcrunPath: "/usr/bin/xcrun",
    xcodebuildPath: "/usr/bin/xcodebuild",
    simctlAvailable: true,
    devicectlAvailable: true,
    };
    describe("myTool", () => {
    beforeEach(() => vi.clearAllMocks());
    it("executes the command correctly", async () => {
    execFile.mockImplementation((_c, _a, _o, cb) => {
    cb(null, "expected output", "");
    });
    const result = await myTool({ requiredParam: "/path/to/file" }, env);
    expect(result.content[0].text).toContain("expected output");
    });
    it("validates absolute path", async () => {
    await expect(
    myTool({ requiredParam: "relative/path" }, env),
    ).rejects.toThrow();
    });
    });
  4. Run the quality gate:

    Terminal window
    npm run check

    All tests must pass, no lint errors, no type errors, formatting correct.

  • Strict mode enabled — no any, no implicit undefined
  • ES modules with .js extensions in imports
  • Target ES2022 — modern JS features are fine
ItemConventionExample
Tool namessnake_casexcode_build, simulator_list
File nameskebab-casebuild-settings.ts, log-stream.ts
FunctionscamelCasexcodeBuild, logStream
InterfacesPascalCaseEnvironment, ExecResult
ConstantsSCREAMING_SNAKEMAX_BUFFER_SIZE
  • Start with a verb: “Build”, “List”, “Run”, “Create”
  • Be specific about what the tool does
  • Mention the underlying CLI tool if relevant
  • Keep it under 100 characters
  • Prettier handles all formatting
  • 100 character line width
  • Double quotes for strings
  • Trailing commas everywhere
  • 2-space indentation
  • ESLint with TypeScript plugin and Prettier integration
  • No unused variables or imports
  • No explicit any
  • Pre-commit hooks enforce lint and format via Husky + lint-staged
  • Vitest for all tests
  • Mock child_process using vi.hoisted() + vi.mock()
  • Never call real Xcode tools in tests

For each tool:

  1. Successful execution — correct command args, proper output parsing
  2. Input validation — invalid paths, missing required params
  3. Error handling — command failures, timeouts
  4. Edge cases — empty output, special characters

Minimum 15% statement coverage is enforced by vitest config, but aim higher. The current test suite has 112 tests across 15 files.

  1. One feature per PR — keep changes focused
  2. Include tests — all new tools must have test coverage
  3. Run npm run check — must pass before submitting
  4. Write a clear description — explain what and why
  5. Clean history — squash WIP commits before review
  • New tool has a handler file in src/tools/<category>/
  • Tool is registered in the category index.ts with Zod schema
  • Tests are written in tests/tools/<category>.test.ts
  • npm run check passes (typecheck, lint, format, tests)
  • PR description explains the purpose and approach