diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 2ca5bffe..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,3 +0,0 @@ -# These are supported funding model platforms -github: cameroncooke -buy_me_a_coffee: cameroncooke diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9b99af3c..9b8e2d31 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -384,7 +384,7 @@ jobs: if: env.HOMEBREW_TAP_TOKEN != '' run: | VERSION="${{ needs.release.outputs.version }}" - FORMULA_BASE_URL="https://github.com/cameroncooke/XcodeBuildMCP/releases/download/v${VERSION}" + FORMULA_BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}" ARM64_SHA="$(awk '{print $1}' dist/portable/arm64/xcodebuildmcp-${VERSION}-darwin-arm64.tar.gz.sha256)" X64_SHA="$(awk '{print $1}' dist/portable/x64/xcodebuildmcp-${VERSION}-darwin-x64.tar.gz.sha256)" npm run homebrew:formula -- \ @@ -401,14 +401,14 @@ jobs: run: | VERSION="${{ needs.release.outputs.version }}" DEFAULT_BRANCH="main" - gh repo clone cameroncooke/homebrew-xcodebuildmcp tap-repo + gh repo clone ${{ github.repository_owner }}/homebrew-xcodebuildmcp tap-repo mkdir -p tap-repo/Formula cp dist/homebrew/Formula/xcodebuildmcp.rb tap-repo/Formula/xcodebuildmcp.rb cd tap-repo git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git remote set-url origin "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/cameroncooke/homebrew-xcodebuildmcp.git" - DETECTED_BRANCH="$(gh repo view cameroncooke/homebrew-xcodebuildmcp --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || true)" + git remote set-url origin "https://x-access-token:${HOMEBREW_TAP_TOKEN}@github.com/${{ github.repository_owner }}/homebrew-xcodebuildmcp.git" + DETECTED_BRANCH="$(gh repo view ${{ github.repository_owner }}/homebrew-xcodebuildmcp --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || true)" if [ -n "$DETECTED_BRANCH" ]; then DEFAULT_BRANCH="$DETECTED_BRANCH" fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 8406be77..6c6e2c60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [2.0.5] +## [2.0.5] - 2026-02-10 ### Added diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 57c6600b..b4bafb44 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,128 +1 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, -diverse, inclusive, and healthy community. - -## Our Standards - -Examples of behavior that contributes to a positive environment for our -community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, - and learning from the experience -* Focusing on what is best not just for us as individuals, but for the - overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or - advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email - address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Enforcement Responsibilities - -Community leaders are responsible for clarifying and enforcing our standards of -acceptable behavior and will take appropriate and fair corrective action in -response to any behavior that they deem inappropriate, threatening, offensive, -or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, and will communicate reasons for moderation -decisions when appropriate. - -## Scope - -This Code of Conduct applies within all community spaces, and also applies when -an individual is officially representing the community in public spaces. -Examples of representing our community include using an official e-mail address, -posting via an official social media account, or acting as an appointed -representative at an online or offline event. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -xcodebuildmcp@cameroncooke.com. -All complaints will be reviewed and investigated promptly and fairly. - -All community leaders are obligated to respect the privacy and security of the -reporter of any incident. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining -the consequences for any action they deem in violation of this Code of Conduct: - -### 1. Correction - -**Community Impact**: Use of inappropriate language or other behavior deemed -unprofessional or unwelcome in the community. - -**Consequence**: A private, written warning from community leaders, providing -clarity around the nature of the violation and an explanation of why the -behavior was inappropriate. A public apology may be requested. - -### 2. Warning - -**Community Impact**: A violation through a single incident or series -of actions. - -**Consequence**: A warning with consequences for continued behavior. No -interaction with the people involved, including unsolicited interaction with -those enforcing the Code of Conduct, for a specified period of time. This -includes avoiding interactions in community spaces as well as external channels -like social media. Violating these terms may lead to a temporary or -permanent ban. - -### 3. Temporary Ban - -**Community Impact**: A serious violation of community standards, including -sustained inappropriate behavior. - -**Consequence**: A temporary ban from any sort of interaction or public -communication with the community for a specified period of time. No public or -private interaction with the people involved, including unsolicited interaction -with those enforcing the Code of Conduct, is allowed during this period. -Violating these terms may lead to a permanent ban. - -### 4. Permanent Ban - -**Community Impact**: Demonstrating a pattern of violation of community -standards, including sustained inappropriate behavior, harassment of an -individual, or aggression toward or disparagement of classes of individuals. - -**Consequence**: A permanent ban from any sort of public interaction within -the community. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], -version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. - -Community Impact Guidelines were inspired by [Mozilla's code of conduct -enforcement ladder](https://github.com/mozilla/diversity). - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +See https://open.sentry.io/code-of-conduct/ diff --git a/README.md b/README.md index 02e024d1..f5b5e7b6 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ A Model Context Protocol (MCP) server and CLI that provides tools for agent use when working on iOS and macOS projects. -[![CI](https://github.com/cameroncooke/XcodeBuildMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/cameroncooke/XcodeBuildMCP/actions/workflows/ci.yml) -[![npm version](https://badge.fury.io/js/xcodebuildmcp.svg)](https://badge.fury.io/js/xcodebuildmcp) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Node.js](https://img.shields.io/badge/node->=18.x-brightgreen.svg)](https://nodejs.org/) [![Xcode 16](https://img.shields.io/badge/Xcode-16-blue.svg)](https://developer.apple.com/xcode/) [![macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](https://www.apple.com/macos/) [![MCP](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/cameroncooke/XcodeBuildMCP) +[![CI](https://github.com/getsentry/XcodeBuildMCP/actions/workflows/ci.yml/badge.svg)](https://github.com/getsentry/XcodeBuildMCP/actions/workflows/ci.yml) +[![npm version](https://badge.fury.io/js/xcodebuildmcp.svg)](https://badge.fury.io/js/xcodebuildmcp) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Node.js](https://img.shields.io/badge/node->=18.x-brightgreen.svg)](https://nodejs.org/) [![Xcode 16](https://img.shields.io/badge/Xcode-16-blue.svg)](https://developer.apple.com/xcode/) [![macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)](https://www.apple.com/macos/) [![MCP](https://img.shields.io/badge/MCP-Compatible-green.svg)](https://modelcontextprotocol.io/) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/getsentry/XcodeBuildMCP) ## Installation @@ -12,7 +12,7 @@ XcodeBuildMCP ships as a single package with two modes: a **CLI** for direct ter ### Option A — Homebrew ```bash -brew tap cameroncooke/xcodebuildmcp +brew tap getsentry/xcodebuildmcp brew install xcodebuildmcp ``` @@ -273,7 +273,7 @@ XcodeBuildMCP now includes two optional agent skills: To install, copy and paste the command below into a terminal and follow the on-screen instructions. ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh ``` For further information on how to install the skill, see: [docs/SKILLS.md](docs/SKILLS.md) diff --git a/config.example.yaml b/config.example.yaml index c2942a3c..7abb4b68 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -17,4 +17,4 @@ sessionDefaults: derivedDataPath: "./.derivedData" preferXcodebuild: false platform: "iOS" - bundleId: "com.example.myapp" + bundleId: "io.sentry.myapp" diff --git a/docs/CLI.md b/docs/CLI.md index d49dcadd..51d227b3 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -68,7 +68,7 @@ xcodebuildmcp simulator boot --simulator-name "iPhone 17 Pro" # Install and launch xcodebuildmcp simulator install --simulator-id --app-path ./build/MyApp.app -xcodebuildmcp simulator launch-app --simulator-id --bundle-id com.example.MyApp +xcodebuildmcp simulator launch-app --simulator-id --bundle-id io.sentry.MyApp # Or... build and run in a single command xcodebuildmcp simulator build-and-run --scheme MyApp --project-path ./MyApp.xcodeproj @@ -78,7 +78,7 @@ xcodebuildmcp simulator build-and-run --scheme MyApp --project-path ./MyApp.xcod ```bash # Start log capture -xcodebuildmcp logging start-simulator-log-capture --simulator-id --bundle-id com.example.MyApp +xcodebuildmcp logging start-simulator-log-capture --simulator-id --bundle-id io.sentry.MyApp > Log capture started successfully. Session ID: 51e2142a-1a99-442a-af01-0586540043df. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 7629e7fa..c8837192 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -54,7 +54,7 @@ sessionDefaults: suppressWarnings: true derivedDataPath: "./.derivedData" preferXcodebuild: false - bundleId: "com.example.myapp" + bundleId: "io.sentry.myapp" # Build settings incrementalBuildsEnabled: false @@ -252,8 +252,8 @@ macosTemplateVersion: "v1.2.3" ``` Default templates: -- iOS: [XcodeBuildMCP-iOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template) -- macOS: [XcodeBuildMCP-macOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template) +- iOS: [XcodeBuildMCP-iOS-Template](https://github.com/getsentry/XcodeBuildMCP-iOS-Template) +- macOS: [XcodeBuildMCP-macOS-Template](https://github.com/getsentry/XcodeBuildMCP-macOS-Template) --- diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 8695b7bd..511259c0 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -23,7 +23,7 @@ Both methods give you the CLI and the MCP server. ### Option A — Homebrew (no Node.js required) ```bash -brew tap cameroncooke/xcodebuildmcp +brew tap getsentry/xcodebuildmcp brew install xcodebuildmcp ``` diff --git a/docs/MIGRATION_V2.md b/docs/MIGRATION_V2.md index b3bbe7d8..9994c923 100644 --- a/docs/MIGRATION_V2.md +++ b/docs/MIGRATION_V2.md @@ -232,7 +232,7 @@ v2.0.0 introduces optional skill files that prime your coding agent with usage i Install via the interactive installer: ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.0/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.0/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh ``` See [SKILLS.md](SKILLS.md) for more details. diff --git a/docs/SKILLS.md b/docs/SKILLS.md index 813357d6..8a8f8261 100644 --- a/docs/SKILLS.md +++ b/docs/SKILLS.md @@ -11,7 +11,7 @@ XcodeBuildMCP now includes two optional agent skills: Install via the interactive installer and follow the on-screen instructions. ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh ``` ## Automated installation @@ -21,19 +21,19 @@ Useful for CI/CD pipelines or for agentic installation. `--skill` should be set ### Install (Claude Code) ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --claude --remove-conflict --skill +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --claude --remove-conflict --skill ``` ### Install (Cursor) ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --cursor --remove-conflict --skill +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --cursor --remove-conflict --skill ``` ### Install (Codex CLI) ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --codex --remove-conflict --skill +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --codex --remove-conflict --skill ``` ### Install (Other Clients) @@ -41,7 +41,7 @@ curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/s For other clients if you know the path to the skills directory you can pass the `--dest` flag. ```bash -curl -fsSL https://raw.githubusercontent.com/cameroncooke/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --dest /path/to/skills --remove-conflict --skill +curl -fsSL https://raw.githubusercontent.com/getsentry/XcodeBuildMCP/v2.0.5/scripts/install-skill.sh -o install-skill.sh && bash install-skill.sh --dest /path/to/skills --remove-conflict --skill ``` ## Unsupporting Clients diff --git a/docs/dev/CLI_CONVERSION_PLAN.md b/docs/dev/CLI_CONVERSION_PLAN.md deleted file mode 100644 index b1b029ff..00000000 --- a/docs/dev/CLI_CONVERSION_PLAN.md +++ /dev/null @@ -1,894 +0,0 @@ -# XcodeBuildMCP CLI Conversion Plan - -This document outlines the architectural plan to convert XcodeBuildMCP into a first-class CLI tool (`xcodebuildcli`) while maintaining full MCP server compatibility. - -## Overview - -### Goals - -1. **First-class CLI**: Separate CLI binary (`xcodebuildcli`) that invokes tools and exits -2. **MCP server unchanged**: `xcodebuildmcp` remains the long-lived stdio MCP server -3. **Shared tool logic**: All three runtimes (MCP, CLI, daemon) invoke the same underlying tool handlers -4. **Session defaults parity**: Identical behavior in all modes -5. **Stateful operation support**: Full daemon architecture for log capture, video recording, debugging, SwiftPM background - -### Non-Goals - -- Breaking existing MCP client integrations -- Changing the MCP protocol or tool schemas -- Wrapping MCP inside CLI (architecturally wrong) - ---- - -## Design Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| CLI Framework | yargs | Better dynamic command generation, strict validation, array support | -| Stateful Support | Full daemon | Unix domain socket for complete multi-step stateful operations | -| Daemon Communication | Unix domain socket | macOS only, simple protocol, reliable | -| Stateful Tools Priority | All equally | Logging, video, debugging, SwiftPM all route to daemon | -| Tool Name Format | kebab-case | CLI-friendly, disambiguated when collisions exist | -| CLI Binary Name | `xcodebuildcli` | Distinct from MCP server binary | - ---- - -## Target Runtime Model - -### Entry Points - -| Binary | Entry Point | Description | -|--------|-------------|-------------| -| `xcodebuildmcp` | `src/index.ts` | MCP server (stdio, long-lived) - unchanged | -| `xcodebuildcli` | `src/cli.ts` | CLI (short-lived, exits after action) | -| Internal | `src/daemon.ts` | Daemon (Unix socket server, long-lived) | - -### Execution Modes - -- **Stateless tools**: CLI runs tools **in-process** by default (fast path) -- **Stateful tools** (log capture, video, debugging, SwiftPM background): CLI routes to **daemon** over Unix domain socket - -### Naming Rules - -- CLI tool names are **kebab-case** -- Internal MCP tool names remain **unchanged** (e.g., `build_sim`, `start_sim_log_cap`) -- CLI tool names are **derived** from MCP tool names, **disambiguated** when duplicates exist - -**Disambiguation rule:** -- If a tool's kebab-name is unique across enabled workflows: use it (e.g., `build-sim`) -- If duplicated across workflows (e.g., `clean` exists in multiple): CLI name becomes `-` (e.g., `simulator-clean`, `device-clean`) - ---- - -## Directory Structure - -### New Files - -``` -src/ - cli.ts # xcodebuildcli entry point (yargs) - daemon.ts # daemon entry point (unix socket server) - runtime/ - bootstrap-runtime.ts # shared runtime bootstrap (config + session defaults) - naming.ts # kebab-case + disambiguation + arg key transforms - tool-catalog.ts # loads workflows/tools, builds ToolCatalog with cliName mapping - tool-invoker.ts # shared "invoke tool by cliName" implementation - types.ts # shared core interfaces (ToolDefinition, ToolCatalog, Invoker) - daemon/ - protocol.ts # daemon protocol types (request/response, errors) - framing.ts # length-prefixed framing helpers for net.Socket - socket-path.ts # resolves default socket path + ensures dirs + cleanup - daemon-server.ts # Unix socket server + request router - cli/ - yargs-app.ts # builds yargs instance, registers commands - daemon-client.ts # CLI -> daemon client (unix socket, protocol) - commands/ - daemon.ts # yargs commands: daemon start/stop/status/restart - tools.ts # yargs command: tools (list available tool commands) - register-tool-commands.ts # auto-register tool commands from schemas - schema-to-yargs.ts # converts Zod schema shape -> yargs options - output.ts # prints ToolResponse to terminal -``` - -### Modified Files - -- `src/server/bootstrap.ts` - Refactor to use shared runtime bootstrap -- `src/core/plugin-types.ts` - Extend `PluginMeta` with optional CLI metadata -- `tsup.config.ts` - Add `cli` and `daemon` entries -- `package.json` - Add `xcodebuildcli` bin, add yargs dependency - ---- - -## Core Interfaces - -### Tool Definition and Catalog - -**File:** `src/runtime/types.ts` - -```typescript -import type * as z from 'zod'; -import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; -import type { ToolResponse } from '../types/common.ts'; -import type { ToolSchemaShape, PluginMeta } from '../core/plugin-types.ts'; - -export type RuntimeKind = 'cli' | 'daemon' | 'mcp'; - -export interface ToolDefinition { - /** Stable CLI command name (kebab-case, disambiguated) */ - cliName: string; - - /** Original MCP tool name as declared today (unchanged) */ - mcpName: string; - - /** Workflow directory name (e.g., "simulator", "device", "logging") */ - workflow: string; - - description?: string; - annotations?: ToolAnnotations; - - /** - * Schema shape used to generate yargs flags for CLI. - * Must include ALL parameters (not the session-default-hidden version). - */ - cliSchema: ToolSchemaShape; - - /** - * Schema shape used for MCP registration (what you already have). - */ - mcpSchema: ToolSchemaShape; - - /** - * Whether CLI MUST route this tool to the daemon (stateful operations). - */ - stateful: boolean; - - /** - * Shared handler (same used by MCP today). No duplication. - */ - handler: PluginMeta['handler']; -} - -export interface ToolCatalog { - tools: ToolDefinition[]; - getByCliName(name: string): ToolDefinition | null; - resolve(input: string): { tool?: ToolDefinition; ambiguous?: string[]; notFound?: boolean }; -} - -export interface InvokeOptions { - runtime: RuntimeKind; - enabledWorkflows?: string[]; - forceDaemon?: boolean; - socketPath?: string; -} - -export interface ToolInvoker { - invoke(toolName: string, args: Record, opts: InvokeOptions): Promise; -} -``` - -### Plugin CLI Metadata Extension - -**File:** `src/core/plugin-types.ts` (modify) - -```typescript -export interface PluginCliMeta { - /** Optional override of derived CLI name */ - name?: string; - /** Full schema shape for CLI flag generation (legacy, includes session-managed fields) */ - schema?: ToolSchemaShape; - /** Mark tool as requiring daemon routing */ - stateful?: boolean; -} - -export interface PluginMeta { - readonly name: string; - readonly schema: ToolSchemaShape; - readonly description?: string; - readonly annotations?: ToolAnnotations; - readonly cli?: PluginCliMeta; // NEW (optional) - handler(params: Record): Promise; -} -``` - -### Daemon Protocol - -**File:** `src/daemon/protocol.ts` - -```typescript -export const DAEMON_PROTOCOL_VERSION = 1 as const; - -export type DaemonMethod = - | 'daemon.status' - | 'daemon.stop' - | 'tool.list' - | 'tool.invoke'; - -export interface DaemonRequest { - v: typeof DAEMON_PROTOCOL_VERSION; - id: string; - method: DaemonMethod; - params?: TParams; -} - -export type DaemonErrorCode = - | 'BAD_REQUEST' - | 'NOT_FOUND' - | 'AMBIGUOUS_TOOL' - | 'TOOL_FAILED' - | 'INTERNAL'; - -export interface DaemonError { - code: DaemonErrorCode; - message: string; - data?: unknown; -} - -export interface DaemonResponse { - v: typeof DAEMON_PROTOCOL_VERSION; - id: string; - result?: TResult; - error?: DaemonError; -} - -export interface ToolInvokeParams { - tool: string; - args: Record; -} - -export interface ToolInvokeResult { - response: unknown; -} - -export interface DaemonStatusResult { - pid: number; - socketPath: string; - startedAt: string; - enabledWorkflows: string[]; - toolCount: number; -} -``` - ---- - -## Shared Runtime Bootstrap - -**File:** `src/runtime/bootstrap-runtime.ts` - -```typescript -import process from 'node:process'; -import { initConfigStore, getConfig, type RuntimeConfigOverrides } from '../utils/config-store.ts'; -import { sessionStore } from '../utils/session-store.ts'; -import { getDefaultFileSystemExecutor } from '../utils/command.ts'; -import type { FileSystemExecutor } from '../utils/FileSystemExecutor.ts'; -import type { RuntimeKind } from './types.ts'; - -export interface BootstrapRuntimeOptions { - runtime: RuntimeKind; - cwd?: string; - fs?: FileSystemExecutor; - configOverrides?: RuntimeConfigOverrides; -} - -export interface BootstrappedRuntime { - runtime: RuntimeKind; - cwd: string; - config: ReturnType; -} - -export async function bootstrapRuntime(opts: BootstrapRuntimeOptions): Promise { - const cwd = opts.cwd ?? process.cwd(); - const fs = opts.fs ?? getDefaultFileSystemExecutor(); - - await initConfigStore({ cwd, fs, overrides: opts.configOverrides }); - - const config = getConfig(); - - const defaults = config.sessionDefaults ?? {}; - if (Object.keys(defaults).length > 0) { - sessionStore.setDefaults(defaults); - } - - return { runtime: opts.runtime, cwd, config }; -} -``` - ---- - -## Tool Catalog - -**File:** `src/runtime/tool-catalog.ts` - -```typescript -import { loadWorkflowGroups } from '../core/plugin-registry.ts'; -import { resolveSelectedWorkflows } from '../utils/workflow-selection.ts'; -import type { ToolCatalog, ToolDefinition } from './types.ts'; -import { toKebabCase, disambiguateCliNames } from './naming.ts'; - -export async function buildToolCatalog(opts: { - enabledWorkflows: string[]; -}): Promise { - const workflowGroups = await loadWorkflowGroups(); - const selection = resolveSelectedWorkflows(opts.enabledWorkflows, workflowGroups); - - const tools: ToolDefinition[] = []; - - for (const wf of selection.selectedWorkflows) { - for (const tool of wf.tools) { - const baseCliName = tool.cli?.name ?? toKebabCase(tool.name); - tools.push({ - cliName: baseCliName, - mcpName: tool.name, - workflow: wf.directoryName, - description: tool.description, - annotations: tool.annotations, - mcpSchema: tool.schema, - cliSchema: tool.cli?.schema ?? tool.schema, - stateful: Boolean(tool.cli?.stateful), - handler: tool.handler, - }); - } - } - - const disambiguated = disambiguateCliNames(tools); - - return { - tools: disambiguated, - getByCliName(name) { - return disambiguated.find((t) => t.cliName === name) ?? null; - }, - resolve(input) { - const exact = disambiguated.filter((t) => t.cliName === input); - if (exact.length === 1) return { tool: exact[0] }; - - const aliasMatches = disambiguated.filter((t) => toKebabCase(t.mcpName) === input); - if (aliasMatches.length === 1) return { tool: aliasMatches[0] }; - if (aliasMatches.length > 1) return { ambiguous: aliasMatches.map((t) => t.cliName) }; - - return { notFound: true }; - }, - }; -} -``` - -**File:** `src/runtime/naming.ts` - -```typescript -import type { ToolDefinition } from './types.ts'; - -export function toKebabCase(name: string): string { - return name - .trim() - .replace(/_/g, '-') - .replace(/\s+/g, '-') - .replace(/[A-Z]/g, (m) => m.toLowerCase()) - .toLowerCase(); -} - -export function disambiguateCliNames(tools: ToolDefinition[]): ToolDefinition[] { - const groups = new Map(); - for (const t of tools) { - groups.set(t.cliName, [...(groups.get(t.cliName) ?? []), t]); - } - - return tools.map((t) => { - const same = groups.get(t.cliName) ?? []; - if (same.length <= 1) return t; - return { ...t, cliName: `${t.workflow}-${t.cliName}` }; - }); -} -``` - ---- - -## Daemon Architecture - -### Socket Path - -**File:** `src/daemon/socket-path.ts` - -```typescript -import { mkdirSync, existsSync, unlinkSync } from 'node:fs'; -import { homedir } from 'node:os'; -import { join } from 'node:path'; - -export function defaultSocketPath(): string { - return join(homedir(), '.xcodebuildcli', 'daemon.sock'); -} - -export function ensureSocketDir(socketPath: string): void { - const dir = socketPath.split('/').slice(0, -1).join('/'); - if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 }); -} - -export function removeStaleSocket(socketPath: string): void { - if (existsSync(socketPath)) unlinkSync(socketPath); -} -``` - -### Length-Prefixed Framing - -**File:** `src/daemon/framing.ts` - -```typescript -import type net from 'node:net'; - -export function writeFrame(socket: net.Socket, obj: unknown): void { - const json = Buffer.from(JSON.stringify(obj), 'utf8'); - const header = Buffer.alloc(4); - header.writeUInt32BE(json.length, 0); - socket.write(Buffer.concat([header, json])); -} - -export function createFrameReader(onMessage: (msg: unknown) => void) { - let buffer = Buffer.alloc(0); - - return (chunk: Buffer) => { - buffer = Buffer.concat([buffer, chunk]); - - while (buffer.length >= 4) { - const len = buffer.readUInt32BE(0); - if (buffer.length < 4 + len) return; - - const payload = buffer.subarray(4, 4 + len); - buffer = buffer.subarray(4 + len); - - const msg = JSON.parse(payload.toString('utf8')); - onMessage(msg); - } - }; -} -``` - -### Daemon Server - -**File:** `src/daemon/daemon-server.ts` - -```typescript -import net from 'node:net'; -import { writeFrame, createFrameReader } from './framing.ts'; -import type { ToolCatalog } from '../runtime/types.ts'; -import type { DaemonRequest, DaemonResponse, ToolInvokeParams } from './protocol.ts'; -import { DAEMON_PROTOCOL_VERSION } from './protocol.ts'; -import { DefaultToolInvoker } from '../runtime/tool-invoker.ts'; - -export interface DaemonServerContext { - socketPath: string; - startedAt: string; - enabledWorkflows: string[]; - catalog: ToolCatalog; -} - -export function startDaemonServer(ctx: DaemonServerContext): net.Server { - const invoker = new DefaultToolInvoker(ctx.catalog); - - const server = net.createServer((socket) => { - const onData = createFrameReader(async (msg) => { - const req = msg as DaemonRequest; - const base = { v: DAEMON_PROTOCOL_VERSION, id: req?.id ?? 'unknown' }; - - try { - if (req.v !== DAEMON_PROTOCOL_VERSION) { - return writeFrame(socket, { ...base, error: { code: 'BAD_REQUEST', message: 'Unsupported protocol version' } }); - } - - switch (req.method) { - case 'daemon.status': - return writeFrame(socket, { - ...base, - result: { - pid: process.pid, - socketPath: ctx.socketPath, - startedAt: ctx.startedAt, - enabledWorkflows: ctx.enabledWorkflows, - toolCount: ctx.catalog.tools.length, - }, - }); - - case 'daemon.stop': - writeFrame(socket, { ...base, result: { ok: true } }); - server.close(() => process.exit(0)); - return; - - case 'tool.list': - return writeFrame(socket, { - ...base, - result: ctx.catalog.tools.map((t) => ({ - name: t.cliName, - workflow: t.workflow, - description: t.description ?? '', - stateful: t.stateful, - })), - }); - - case 'tool.invoke': { - const params = req.params as ToolInvokeParams; - const response = await invoker.invoke(params.tool, params.args ?? {}, { - runtime: 'daemon', - enabledWorkflows: ctx.enabledWorkflows, - }); - return writeFrame(socket, { ...base, result: { response } }); - } - - default: - return writeFrame(socket, { ...base, error: { code: 'BAD_REQUEST', message: `Unknown method` } }); - } - } catch (error) { - return writeFrame(socket, { - ...base, - error: { code: 'INTERNAL', message: error instanceof Error ? error.message : String(error) }, - }); - } - }); - - socket.on('data', onData); - }); - - return server; -} -``` - -### Daemon Entry Point - -**File:** `src/daemon.ts` - -```typescript -#!/usr/bin/env node -import net from 'node:net'; -import { bootstrapRuntime } from './runtime/bootstrap-runtime.ts'; -import { buildToolCatalog } from './runtime/tool-catalog.ts'; -import { ensureSocketDir, defaultSocketPath, removeStaleSocket } from './daemon/socket-path.ts'; -import { startDaemonServer } from './daemon/daemon-server.ts'; - -async function main(): Promise { - const runtime = await bootstrapRuntime({ runtime: 'daemon' }); - const socketPath = process.env.XCODEBUILDCLI_SOCKET ?? defaultSocketPath(); - - ensureSocketDir(socketPath); - - try { - await new Promise((resolve, reject) => { - const s = net.createConnection(socketPath, () => { - s.end(); - reject(new Error('Daemon already running')); - }); - s.on('error', () => resolve()); - }); - } catch (e) { - throw e; - } - - removeStaleSocket(socketPath); - - const catalog = await buildToolCatalog({ enabledWorkflows: runtime.config.enabledWorkflows }); - - const server = startDaemonServer({ - socketPath, - startedAt: new Date().toISOString(), - enabledWorkflows: runtime.config.enabledWorkflows, - catalog, - }); - - server.listen(socketPath); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); -``` - ---- - -## CLI Architecture - -### CLI Entry Point - -**File:** `src/cli.ts` - -```typescript -#!/usr/bin/env node -import { bootstrapRuntime } from './runtime/bootstrap-runtime.ts'; -import { buildToolCatalog } from './runtime/tool-catalog.ts'; -import { buildYargsApp } from './cli/yargs-app.ts'; - -async function main(): Promise { - const runtime = await bootstrapRuntime({ runtime: 'cli' }); - const catalog = await buildToolCatalog({ enabledWorkflows: runtime.config.enabledWorkflows }); - - const yargsApp = buildYargsApp({ catalog, runtimeConfig: runtime.config }); - await yargsApp.parseAsync(); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); -``` - -### Yargs App - -**File:** `src/cli/yargs-app.ts` - -```typescript -import yargs from 'yargs'; -import { hideBin } from 'yargs/helpers'; -import type { ToolCatalog } from '../runtime/types.ts'; -import { registerDaemonCommands } from './commands/daemon.ts'; -import { registerToolsCommand } from './commands/tools.ts'; -import { registerToolCommands } from './register-tool-commands.ts'; -import { version } from '../version.ts'; - -export function buildYargsApp(opts: { - catalog: ToolCatalog; - runtimeConfig: { enabledWorkflows: string[] }; -}) { - const app = yargs(hideBin(process.argv)) - .scriptName('xcodebuildcli') - .strict() - .recommendCommands() - .wrap(Math.min(120, yargs.terminalWidth())) - .parserConfiguration({ - 'camel-case-expansion': true, - 'strip-dashed': true, - }) - .option('socket', { - type: 'string', - describe: 'Override daemon unix socket path', - default: process.env.XCODEBUILDCLI_SOCKET, - }) - .option('daemon', { - type: 'boolean', - describe: 'Force daemon execution even for stateless tools', - default: false, - }) - .version(version) - .help(); - - registerDaemonCommands(app); - registerToolsCommand(app, opts.catalog); - registerToolCommands(app, opts.catalog); - - return app; -} -``` - -### Schema to Yargs Conversion - -**File:** `src/cli/schema-to-yargs.ts` - -```typescript -import * as z from 'zod'; - -export type YargsOpt = - | { type: 'string'; array?: boolean; choices?: string[]; describe?: string } - | { type: 'number'; array?: boolean; describe?: string } - | { type: 'boolean'; describe?: string }; - -function unwrap(t: z.ZodTypeAny): z.ZodTypeAny { - if (t instanceof z.ZodOptional) return unwrap(t.unwrap()); - if (t instanceof z.ZodNullable) return unwrap(t.unwrap()); - if (t instanceof z.ZodDefault) return unwrap(t.removeDefault()); - if (t instanceof z.ZodEffects) return unwrap(t.innerType()); - return t; -} - -export function zodToYargsOption(t: z.ZodTypeAny): YargsOpt | null { - const u = unwrap(t); - - if (u instanceof z.ZodString) return { type: 'string' }; - if (u instanceof z.ZodNumber) return { type: 'number' }; - if (u instanceof z.ZodBoolean) return { type: 'boolean' }; - - if (u instanceof z.ZodEnum) return { type: 'string', choices: u.options }; - if (u instanceof z.ZodNativeEnum) return { type: 'string', choices: Object.values(u.enum) as string[] }; - - if (u instanceof z.ZodArray) { - const inner = unwrap(u.element); - if (inner instanceof z.ZodString) return { type: 'string', array: true }; - if (inner instanceof z.ZodNumber) return { type: 'number', array: true }; - return null; - } - - return null; -} -``` - -### Tool Command Registration - -**File:** `src/cli/register-tool-commands.ts` - -```typescript -import type { Argv } from 'yargs'; -import type { ToolCatalog } from '../runtime/types.ts'; -import { DefaultToolInvoker } from '../runtime/tool-invoker.ts'; -import { zodToYargsOption } from './schema-to-yargs.ts'; -import { toKebabCase } from '../runtime/naming.ts'; -import { printToolResponse } from './output.ts'; - -export function registerToolCommands(app: Argv, catalog: ToolCatalog): void { - const invoker = new DefaultToolInvoker(catalog); - - for (const tool of catalog.tools) { - app.command( - tool.cliName, - tool.description ?? '', - (y) => { - y.option('json', { - type: 'string', - describe: 'JSON object of tool args (merged with flags)', - }); - - for (const [key, zt] of Object.entries(tool.cliSchema)) { - const opt = zodToYargsOption(zt as z.ZodTypeAny); - if (!opt) continue; - - const flag = toKebabCase(key); - y.option(flag, { - type: opt.type, - array: (opt as { array?: boolean }).array, - choices: (opt as { choices?: string[] }).choices, - describe: (opt as { describe?: string }).describe, - }); - } - - return y; - }, - async (argv) => { - const { json, socket, daemon, _, $0, ...rest } = argv as Record; - - const jsonArgs = json ? (JSON.parse(String(json)) as Record) : {}; - const flagArgs = rest as Record; - const args = { ...flagArgs, ...jsonArgs }; - - const response = await invoker.invoke(tool.cliName, args, { - runtime: 'cli', - forceDaemon: Boolean(daemon), - socketPath: socket as string | undefined, - }); - - printToolResponse(response); - }, - ); - } -} -``` - -### CLI Output - -**File:** `src/cli/output.ts` - -```typescript -import type { ToolResponse } from '../types/common.ts'; - -export function printToolResponse(res: ToolResponse): void { - for (const item of res.content ?? []) { - if (item.type === 'text') { - console.log(item.text); - } else if (item.type === 'image') { - console.log(`[image ${item.mimeType}, ${item.data.length} bytes base64]`); - } - } - if (res.isError) process.exitCode = 1; -} -``` - ---- - -## Build Configuration - -### tsup.config.ts - -```typescript -export default defineConfig({ - entry: { - index: 'src/index.ts', - 'doctor-cli': 'src/doctor-cli.ts', - cli: 'src/cli.ts', - daemon: 'src/daemon.ts', - }, - // ...existing config... -}); -``` - -### package.json - -```json -{ - "bin": { - "xcodebuildmcp": "build/cli.js", - "xcodebuildmcp-doctor": "build/doctor-cli.js", - "xcodebuildcli": "build/cli.js" - }, - "dependencies": { - "yargs": "^17.7.2" - } -} -``` - ---- - -## Implementation Phases - -### Phase 1: Foundation - -1. Add `src/runtime/bootstrap-runtime.ts` -2. Refactor `src/server/bootstrap.ts` to call shared bootstrap -3. Add `src/cli.ts`, `src/daemon.ts` entries to `tsup.config.ts` -4. Add `xcodebuildcli` bin + `yargs` dependency in `package.json` - -**Result:** Builds produce `build/cli.js` and `build/daemon.js`, MCP server unchanged. - -### Phase 2: Tool Catalog + Direct CLI Invocation (Stateless) - -1. Implement `src/runtime/naming.ts`, `src/runtime/tool-catalog.ts`, `src/runtime/tool-invoker.ts`, `src/runtime/types.ts` -2. Implement `src/cli/yargs-app.ts`, `src/cli/schema-to-yargs.ts`, `src/cli/register-tool-commands.ts`, `src/cli/output.ts` -3. Add `xcodebuildcli tools` list command - -**Result:** `xcodebuildcli ` works for stateless tools in-process. - -### Phase 3: Daemon Protocol + Server + Client - -1. Implement `src/daemon/protocol.ts`, `src/daemon/framing.ts`, `src/daemon/socket-path.ts` -2. Implement `src/daemon/daemon-server.ts` and wire into `src/daemon.ts` -3. Implement `src/cli/daemon-client.ts` -4. Implement `xcodebuildcli daemon start|stop|status|restart` - -**Result:** Daemon starts, responds to status, can invoke tools. - -### Phase 4: Stateful Routing - -1. Add `cli.stateful = true` metadata to all stateful tools (logging, video, debugging, swift-package background) -2. Modify `DefaultToolInvoker` to require daemon when `tool.stateful === true` -3. Add CLI auto-start behavior: if daemon required and not running, start it programmatically - -**Result:** Stateful commands run through daemon reliably; state persists across CLI invocations. - -### Phase 5: Full CLI Schema Coverage - -1. For all tools, ensure `tool.cli.schema` is present and complete -2. Ensure schema-to-yargs supports all Zod types used (string/number/boolean/enum/array) -3. Require complex/nested values via `--json` fallback - -**Result:** CLI is first-class with full native flags. - ---- - -## Command Examples - -```bash -# List available tools -xcodebuildcli tools - -# Run stateless tool with native flags -xcodebuildcli build-sim --scheme MyApp --project-path ./App.xcodeproj - -# Run tool with JSON input -xcodebuildcli build-sim --json '{"scheme":"MyApp"}' - -# Daemon management -xcodebuildcli daemon start -xcodebuildcli daemon status -xcodebuildcli daemon stop - -# Stateful tools (automatically route to daemon) -xcodebuildcli start-sim-log-cap --simulator-id ABCD-1234 -xcodebuildcli stop-sim-log-cap --session-id xyz - -# Force daemon execution for any tool -xcodebuildcli build-sim --daemon --scheme MyApp - -# Help -xcodebuildcli --help -xcodebuildcli build-sim --help -``` - ---- - -## Invariants - -1. **MCP unchanged**: `xcodebuildmcp` continues to work exactly as before -2. **Build/runtime separation unchanged**: MCP and CLI continue to use shared tool handlers -3. **No code duplication**: CLI invokes same `PluginMeta.handler` functions -4. **Session defaults identical**: All runtimes use `bootstrapRuntime()` → `sessionStore` -5. **Tool logic shared**: `src/mcp/tools/*` remains single source of truth -6. **Daemon is macOS-only**: Uses Unix domain sockets; CLI fails with clear error on non-macOS diff --git a/docs/dev/CONTRIBUTING.md b/docs/dev/CONTRIBUTING.md index 65bc3dce..fd5809f4 100644 --- a/docs/dev/CONTRIBUTING.md +++ b/docs/dev/CONTRIBUTING.md @@ -342,8 +342,8 @@ XcodeBuildMCP uses external template repositories for the iOS and macOS project #### Template Repositories -- **iOS Template**: [XcodeBuildMCP-iOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template) -- **macOS Template**: [XcodeBuildMCP-macOS-Template](https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template) +- **iOS Template**: [XcodeBuildMCP-iOS-Template](https://github.com/getsentry/XcodeBuildMCP-iOS-Template) +- **macOS Template**: [XcodeBuildMCP-macOS-Template](https://github.com/getsentry/XcodeBuildMCP-macOS-Template) #### Local Template Development @@ -351,8 +351,8 @@ When developing or testing changes to the templates: 1. Clone the template repository you want to work on: ```bash - git clone https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template.git - git clone https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template.git + git clone https://github.com/getsentry/XcodeBuildMCP-iOS-Template.git + git clone https://github.com/getsentry/XcodeBuildMCP-macOS-Template.git ``` 2. Set the appropriate environment variable to use your local template: diff --git a/docs/dev/PLUGIN_DEVELOPMENT.md b/docs/dev/PLUGIN_DEVELOPMENT.md deleted file mode 100644 index 13d42efe..00000000 --- a/docs/dev/PLUGIN_DEVELOPMENT.md +++ /dev/null @@ -1,769 +0,0 @@ -# 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](#overview) -2. [Plugin Architecture](#plugin-architecture) -3. [Creating New Tools](#creating-new-tools) -4. [Creating New Workflow Groups](#creating-new-workflow-groups) -5. [Creating MCP Resources](#creating-mcp-resources) -6. [Auto-Discovery System](#auto-discovery-system) -7. [Testing Guidelines](#testing-guidelines) -8. [Development Workflow](#development-workflow) -9. [Best Practices](#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: - -```typescript -// 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 { - // 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): Promise => { - 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` | Zod validation schema for parameters | -| `handler` | `function` | Async function: `(args) => Promise` | - -### 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: - -```typescript -// 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: - -```typescript -// 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:** - -```typescript -// 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: - -```typescript -// 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: - -```typescript -// 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: - -```typescript -// 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__/`: - -```typescript -// 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 - -```typescript -// 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. The `workflow-discovery` workflow is only auto-included when `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY=true` (or when explicitly listed). - -Example: -```bash -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:** -```typescript -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):** -```typescript -// ❌ 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 - -```typescript -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 - -```bash -# 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 - -```bash -# 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](../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:** - -```bash -# 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](../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:** -```markdown -- **Additional Tools**: Simulator management, logging, UI testing tools -``` - -**✅ Correct:** -```markdown -- `boot_sim`, `install_app_sim`, `launch_app_sim`, `list_sims`, `open_sim` -- `snapshot_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: - -```markdown -### 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](../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. diff --git a/docs/dev/PROJECT_CONFIG_PLAN.md b/docs/dev/PROJECT_CONFIG_PLAN.md deleted file mode 100644 index 66327422..00000000 --- a/docs/dev/PROJECT_CONFIG_PLAN.md +++ /dev/null @@ -1,139 +0,0 @@ -# Project Config + Runtime Config Store Plan - -## Goal -Add a project-level config file at `.xcodebuildmcp/config.yaml` and a global runtime config store that: -1. Seeds session defaults at server startup (no client call required). -2. Supports **all** configuration options in config.yaml (not just session defaults). -3. Uses environment variables as defaults for any unset config fields. -4. Exposes a single source of truth for configuration reads and persistence. - -Scope is limited to **cwd-only** resolution, **patch-only persistence** (provided keys only), and **warn+ignore** on invalid config. - -## Decisions (Confirmed) -- Config location: **only** `process.cwd()/.xcodebuildmcp/config.yaml` (no find-up). -- Persistence: **only** keys provided in the `session-set-defaults` call (plus necessary deletions for mutual exclusivity). -- Invalid config: **warn and ignore**, continue startup. -- Config format: **flat** keys for everything except `sessionDefaults`. -- Workflow selection: `enabledWorkflows` is optional; when resolved, an empty array means "load all workflows". -- Config store pre-init: **safe defaults** (env + hardcoded) until initialized. - -## Config Format - -Proposed YAML (flat except `sessionDefaults`): - -```yaml -schemaVersion: 1 -enabledWorkflows: ["simulator"] -debug: false -experimentalWorkflowDiscovery: false -disableSessionDefaults: false -uiDebuggerGuardMode: "warn" -incrementalBuildsEnabled: false -dapRequestTimeoutMs: 30000 -dapLogEvents: false -launchJsonWaitMs: 8000 -axePath: "/opt/axe/bin/axe" -iosTemplatePath: "/path/to/ios/templates" -iosTemplateVersion: "v1.2.3" -macosTemplatePath: "/path/to/macos/templates" -macosTemplateVersion: "v1.2.3" -debuggerBackend: "dap" -sessionDefaults: - projectPath: "./MyApp.xcodeproj" - workspacePath: "./MyApp.xcworkspace" - scheme: "MyApp" - configuration: "Debug" - simulatorName: "iPhone 16" - simulatorId: "" - deviceId: "" - useLatestOS: true - arch: "arm64" - suppressWarnings: false - derivedDataPath: "./.derivedData" - preferXcodebuild: false - platform: "iOS" - bundleId: "com.example.myapp" -``` - -Notes: -- `schemaVersion` supports future evolution. -- The config file is **not** exclusive to session defaults; more flat keys can be added as needed. -- Relative paths resolve against the workspace root (cwd). - -## Precedence (Operational) -Runtime config precedence for all non-session defaults: -1. Programmatic overrides -2. Config file (`.xcodebuildmcp/config.yaml`) -3. Environment variables -4. Hardcoded defaults - -Session defaults precedence during tool calls remains: -1. Tool call args (existing behavior in `createSessionAwareTool`) -2. In-memory session defaults (seeded from config, mutable via `session-set-defaults`) - -## Implementation Plan - -### 1) Unified config schema (flat + sessionDefaults) -**File:** `src/utils/runtime-config-schema.ts` (new) -- Define Zod schema for all supported config keys. -- Keep `sessionDefaults` shape from `session-defaults-schema.ts`. -- Allow unknown top-level keys via `.passthrough()`. - -### 2) Expand project config loader/writer -**File:** `src/utils/project-config.ts` -Responsibilities: -- Resolve config path: `path.join(cwd, '.xcodebuildmcp', 'config.yaml')`. -- Read YAML via `FileSystemExecutor`. -- Parse and validate with unified schema. -- Normalize mutual exclusivity in `sessionDefaults`. -- Normalize `enabledWorkflows` into a string[]; preserve "unset" at the config file level. -- Resolve relative paths for `projectPath`, `workspacePath`, and `derivedDataPath`. -- Persist changes when requested: - - Deep-merge patch keys into existing config. - - Remove keys explicitly cleared (e.g., exclusivity deletions). - - Preserve unknown keys. - -### 3) Global config store (single source of truth) -**File:** `src/utils/config-store.ts` (new) -- Build resolved runtime config with precedence: - - overrides > config.yaml > env vars > defaults -- Provide `initConfigStore(...)` and `getConfig()` APIs. -- Provide a persistence helper for session defaults patches that updates config.yaml and the in-memory store. - -### 4) Startup initialization -**File:** `src/server/bootstrap.ts` -- Initialize config store early with `cwd` + `fs`. -- Seed `sessionStore` from `config.sessionDefaults`. -- Use `config.enabledWorkflows`; empty array means "load all". - -### 5) Replace env reads with config store lookups -**Files:** `workflow-selection.ts`, `environment.ts`, `xcodemake.ts`, -`template-manager.ts`, `axe-helpers.ts`, `debugger-manager.ts` -- Keep existing behaviors, but route through config store. -- Env vars remain as defaults through the store. - -### 6) Persist via config store -**File:** `src/mcp/tools/session-management/session_set_defaults.ts` -- Persist session defaults through config store API. -- Keep `deleteKeys` for mutual exclusivity. - -### 7) Runtime overrides -**File:** runtime entrypoints -- Pass overrides into bootstrap/config store, so explicit runtime overrides have the highest precedence. - -### 8) Documentation updates -- Update `docs/CONFIGURATION.md`, `docs/GETTING_STARTED.md`, `docs/SESSION_DEFAULTS.md`. - -## Tests -This change **must** be built TDD (red → green): write failing tests first, then implement code until tests pass. - -Red tests to add before implementation: -- `project-config` loader: flat keys + env fallback + enabledWorkflows normalization. -- `project-config` persistence: deep-merge patch + delete keys + preserve unknown keys. -- `config-store` resolution order: env vs config.yaml vs overrides. -- `session_set_defaults` persistence via config store. - -## Risks / Notes -- Overwriting YAML drops comments and custom formatting. -- Explicitly **cwd-only** prevents automatic discovery from subdirectories. -- Warn+ignore avoids startup failures but can hide misconfigurations; add clear log messaging. diff --git a/docs/dev/RELEASE_PROCESS.md b/docs/dev/RELEASE_PROCESS.md index 3ac65adc..b75e75ad 100644 --- a/docs/dev/RELEASE_PROCESS.md +++ b/docs/dev/RELEASE_PROCESS.md @@ -28,7 +28,7 @@ Production release behavior: - Creates GitHub release and uploads npm tarball. - Builds and verifies portable macOS artifacts (`arm64`, `x64`, `universal`). - Uploads portable artifacts to GitHub release assets. -- Updates the Homebrew tap repository (`cameroncooke/homebrew-xcodebuildmcp`) directly when `HOMEBREW_TAP_TOKEN` is configured. +- Updates the Homebrew tap repository (`getsentry/homebrew-xcodebuildmcp`) directly when `HOMEBREW_TAP_TOKEN` is configured. - Attempts MCP Registry publish (best effort based on configured secrets). ### Manual dispatch (`workflow_dispatch`) diff --git a/docs/dev/RELOADEROO.md b/docs/dev/RELOADEROO.md deleted file mode 100644 index 8d656640..00000000 --- a/docs/dev/RELOADEROO.md +++ /dev/null @@ -1,446 +0,0 @@ -# Reloaderoo Integration Guide - -This guide explains how to use Reloaderoo v1.1.2+ for testing and developing XcodeBuildMCP with both CLI inspection tools and transparent proxy capabilities. - -## Overview - -**Reloaderoo** is a dual-mode MCP development tool that operates as both a CLI inspection tool and a transparent proxy server for the Model Context Protocol (MCP). It provides two distinct operational modes for different development workflows. - -## Installation - -Reloaderoo is available via npm and can be used with npx for universal compatibility. - -```bash -# Use npx to run reloaderoo (works on any system) -npx reloaderoo@latest --help - -# Or install globally if preferred -npm install -g reloaderoo -reloaderoo --help -``` - -## Two Operational Modes - -### 🔍 **CLI Mode** (Inspection & Testing) - -Direct command-line access to MCP servers without client setup - perfect for testing and debugging: - -**Key Benefits:** -- ✅ **One-shot commands** - Test tools, list resources, get server info -- ✅ **No MCP client required** - Perfect for testing and debugging -- ✅ **Raw JSON output** - Ideal for scripts and automation -- ✅ **8 inspection commands** - Complete MCP protocol coverage -- ✅ **AI agent friendly** - Designed for terminal-based AI development workflows - -**Basic Commands:** - -```bash -# List all available tools -npx reloaderoo@latest inspect list-tools -- node build/cli.js mcp - -# Call any tool with parameters -npx reloaderoo@latest inspect call-tool --params '' -- node build/cli.js mcp - -# Get server information -npx reloaderoo@latest inspect server-info -- node build/cli.js mcp - -# List available resources -npx reloaderoo@latest inspect list-resources -- node build/cli.js mcp - -# Read a specific resource -npx reloaderoo@latest inspect read-resource "" -- node build/cli.js mcp - -# List available prompts -npx reloaderoo@latest inspect list-prompts -- node build/cli.js mcp - -# Get a specific prompt -npx reloaderoo@latest inspect get-prompt --args '' -- node build/cli.js mcp - -# Check server connectivity -npx reloaderoo@latest inspect ping -- node build/cli.js mcp -``` - -**Example Tool Calls:** - -```bash -# List connected devices -npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/cli.js mcp - -# Get doctor information -npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/cli.js mcp - -# List iOS simulators -npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/cli.js mcp - -# Read devices resource -npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/cli.js mcp -``` - -### 🔄 **Proxy Mode** (Hot-Reload Development) - -Transparent MCP proxy server that enables seamless hot-reloading during development: - -**Key Benefits:** -- ✅ **Hot-reload MCP servers** without disconnecting your AI client -- ✅ **Session persistence** - Keep your development context intact -- ✅ **Automatic `restart_server` tool** - AI agents can restart servers on demand -- ✅ **Transparent forwarding** - Full MCP protocol passthrough -- ✅ **Process management** - Spawns, monitors, and restarts your server process - -**Usage:** - -```bash -# Start proxy mode (your AI client connects to this) -npx reloaderoo@latest proxy -- node build/cli.js mcp - -# With debug logging -npx reloaderoo@latest proxy --log-level debug -- node build/cli.js mcp - -# Then in your AI session, request: -# "Please restart the MCP server to load my latest changes" -``` - -The AI agent will automatically call the `restart_server` tool, preserving your session while reloading code changes. - -## MCP Inspection Server Mode - -Start CLI mode as a persistent MCP server for interactive debugging through MCP clients: - -```bash -# Start reloaderoo in CLI mode as an MCP server -npx reloaderoo@latest inspect mcp -- node build/cli.js mcp -``` - -This runs CLI mode as a persistent MCP server, exposing 8 debug tools through the MCP protocol: -- `list_tools` - List all server tools -- `call_tool` - Call any server tool -- `list_resources` - List all server resources -- `read_resource` - Read any server resource -- `list_prompts` - List all server prompts -- `get_prompt` - Get any server prompt -- `get_server_info` - Get comprehensive server information -- `ping` - Test server connectivity - -## Claude Code Compatibility - -When running under Claude Code, XcodeBuildMCP automatically detects the environment and consolidates multiple content blocks into single responses with `---` separators. - -**Automatic Detection Methods:** -1. **Environment Variables**: `CLAUDECODE=1` or `CLAUDE_CODE_ENTRYPOINT=cli` -2. **Parent Process Analysis**: Checks if parent process contains 'claude' -3. **Graceful Fallback**: Falls back to environment variables if process detection fails - -**No Configuration Required**: The consolidation happens automatically when Claude Code is detected. - -## Command Reference - -### Command Structure - -```bash -npx reloaderoo@latest [options] [command] - -Two modes, one tool: -• Proxy MCP server that adds support for hot-reloading MCP servers. -• CLI tool for inspecting MCP servers. - -Global Options: - -V, --version Output the version number - -h, --help Display help for command - -Commands: - proxy [options] 🔄 Run as MCP proxy server (default behavior) - inspect 🔍 Inspect and debug MCP servers - info [options] 📊 Display version and configuration information - help [command] ❓ Display help for command -``` - -### 🔄 **Proxy Mode Commands** - -```bash -npx reloaderoo@latest proxy [options] -- [child-args...] - -Options: - -w, --working-dir Working directory for the child process - -l, --log-level Log level (debug, info, notice, warning, error, critical) - -f, --log-file Custom log file path (logs to stderr by default) - -t, --restart-timeout Timeout for restart operations (default: 30000ms) - -m, --max-restarts Maximum restart attempts (0-10, default: 3) - -d, --restart-delay Delay between restart attempts (default: 1000ms) - -q, --quiet Suppress non-essential output - --no-auto-restart Disable automatic restart on crashes - --debug Enable debug mode with verbose logging - --dry-run Validate configuration without starting proxy - -Examples: - npx reloaderoo proxy -- node build/cli.js mcp - npx reloaderoo -- node build/cli.js mcp # Same as above (proxy is default) - npx reloaderoo proxy --log-level debug -- node build/cli.js mcp -``` - -### 🔍 **CLI Mode Commands** - -```bash -npx reloaderoo@latest inspect [subcommand] [options] -- [child-args...] - -Subcommands: - server-info [options] Get server information and capabilities - list-tools [options] List all available tools - call-tool [options] Call a specific tool - list-resources [options] List all available resources - read-resource [options] Read a specific resource - list-prompts [options] List all available prompts - get-prompt [options] Get a specific prompt - ping [options] Check server connectivity - -Examples: - npx reloaderoo@latest inspect list-tools -- node build/cli.js mcp - npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/cli.js mcp - npx reloaderoo@latest inspect server-info -- node build/cli.js mcp -``` - -### **Info Command** - -```bash -npx reloaderoo@latest info [options] - -Options: - -v, --verbose Show detailed information - -h, --help Display help for command - -Examples: - npx reloaderoo@latest info # Show basic system information - npx reloaderoo@latest info --verbose # Show detailed system information -``` - -### Response Format - -All CLI commands return structured JSON: - -```json -{ - "success": true, - "data": { - // Command-specific response data - }, - "metadata": { - "command": "call-tool:list_devices", - "timestamp": "2025-07-25T08:32:47.042Z", - "duration": 1782 - } -} -``` - -### Error Handling - -When commands fail, you'll receive: - -```json -{ - "success": false, - "error": { - "message": "Error description", - "code": "ERROR_CODE" - }, - "metadata": { - "command": "failed-command", - "timestamp": "2025-07-25T08:32:47.042Z", - "duration": 100 - } -} -``` - -## Development Workflow - -### 🔍 **CLI Mode Workflow** (Testing & Debugging) - -Perfect for testing individual tools or debugging server issues without MCP client setup: - -```bash -# 1. Build XcodeBuildMCP -npm run build - -# 2. Test your server quickly -npx reloaderoo@latest inspect list-tools -- node build/cli.js mcp - -# 3. Call specific tools to verify behavior -npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/cli.js mcp - -# 4. Check server health and resources -npx reloaderoo@latest inspect ping -- node build/cli.js mcp -npx reloaderoo@latest inspect list-resources -- node build/cli.js mcp -``` - -### 🔄 **Proxy Mode Workflow** (Hot-Reload Development) - -For full development sessions with AI clients that need persistent connections: - -#### 1. **Start Development Session** -Configure your AI client to connect to reloaderoo proxy instead of your server directly: -```bash -npx reloaderoo@latest proxy -- node build/cli.js mcp -# or with debug logging: -npx reloaderoo@latest proxy --log-level debug -- node build/cli.js mcp -``` - -#### 2. **Develop Your MCP Server** -Work on your XcodeBuildMCP code as usual - make changes, add tools, modify functionality. - -#### 3. **Test Changes Instantly** -```bash -# Rebuild your changes -npm run build - -# Then ask your AI agent to restart the server: -# "Please restart the MCP server to load my latest changes" -``` - -The agent will call the `restart_server` tool automatically. Your new capabilities are immediately available! - -#### 4. **Continue Development** -Your AI session continues with the updated server capabilities. No connection loss, no context reset. - -### 🛠️ **MCP Inspection Server** (Interactive CLI Debugging) - -For interactive debugging through MCP clients: - -```bash -# Start reloaderoo CLI mode as an MCP server -npx reloaderoo@latest inspect mcp -- node build/cli.js mcp - -# Then connect with an MCP client to access debug tools -# Available tools: list_tools, call_tool, list_resources, etc. -``` - -## Troubleshooting - -### 🔄 **Proxy Mode Issues** - -**Server won't start in proxy mode:** -```bash -# Check if XcodeBuildMCP runs independently first -node build/cli.js mcp - -# Then try with reloaderoo proxy to validate configuration -npx reloaderoo@latest proxy -- node build/cli.js mcp -``` - -**Connection problems with MCP clients:** -```bash -# Enable debug logging to see what's happening -npx reloaderoo@latest proxy --log-level debug -- node build/cli.js mcp - -# Check system info and configuration -npx reloaderoo@latest info --verbose -``` - -**Restart failures in proxy mode:** -```bash -# Increase restart timeout -npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/cli.js mcp - -# Check restart limits -npx reloaderoo@latest proxy --max-restarts 5 -- node build/cli.js mcp -``` - -### 🔍 **CLI Mode Issues** - -**CLI commands failing:** -```bash -# Test basic connectivity first -npx reloaderoo@latest inspect ping -- node build/cli.js mcp - -# Enable debug logging for CLI commands (via proxy debug mode) -npx reloaderoo@latest proxy --log-level debug -- node build/cli.js mcp -``` - -**JSON parsing errors:** -```bash -# Check server information for troubleshooting -npx reloaderoo@latest inspect server-info -- node build/cli.js mcp - -# Ensure your server outputs valid JSON -node build/cli.js mcp | head -10 -``` - -### **General Issues** - -**Command not found:** -```bash -# Ensure npx can find reloaderoo -npx reloaderoo@latest --help - -# If that fails, try installing globally -npm install -g reloaderoo -``` - -**Parameter validation:** -```bash -# Ensure JSON parameters are properly quoted -npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/cli.js mcp -``` - -### **General Debug Mode** - -```bash -# Get detailed information about what's happening -npx reloaderoo@latest proxy --debug -- node build/cli.js mcp # For proxy mode -npx reloaderoo@latest proxy --log-level debug -- node build/cli.js mcp # For detailed proxy logging - -# View system information -npx reloaderoo@latest info --verbose -``` - -### Debug Tips - -1. **Always build first**: Run `npm run build` before testing -2. **Check tool names**: Use `inspect list-tools` to see exact tool names -3. **Validate JSON**: Ensure parameters are valid JSON strings -4. **Enable debug logging**: Use `--log-level debug` or `--debug` for verbose output -5. **Test connectivity**: Use `inspect ping` to verify server communication - -## Advanced Usage - -### Environment Variables - -Configure reloaderoo behavior via environment variables: - -```bash -# Logging Configuration -export MCPDEV_PROXY_LOG_LEVEL=debug # Log level (debug, info, notice, warning, error, critical) -export MCPDEV_PROXY_LOG_FILE=/path/to/log # Custom log file path (default: stderr) -export MCPDEV_PROXY_DEBUG_MODE=true # Enable debug mode (true/false) - -# Process Management -export MCPDEV_PROXY_RESTART_LIMIT=5 # Maximum restart attempts (0-10, default: 3) -export MCPDEV_PROXY_AUTO_RESTART=true # Enable/disable auto-restart (true/false) -export MCPDEV_PROXY_TIMEOUT=30000 # Operation timeout in milliseconds -export MCPDEV_PROXY_RESTART_DELAY=1000 # Delay between restart attempts in milliseconds -export MCPDEV_PROXY_CWD=/path/to/directory # Default working directory -``` - -### Custom Working Directory - -```bash -npx reloaderoo@latest proxy --working-dir /custom/path -- node build/cli.js mcp -npx reloaderoo@latest inspect list-tools --working-dir /custom/path -- node build/cli.js mcp -``` - -### Timeout Configuration - -```bash -npx reloaderoo@latest proxy --restart-timeout 60000 -- node build/cli.js mcp -``` - -## Integration with XcodeBuildMCP - -Reloaderoo is specifically configured to work with XcodeBuildMCP's: - -- **84+ Tools**: All workflow groups accessible via CLI -- **4 Resources**: Direct access to devices, simulators, environment, swift-packages -- **Claude Code Detection**: Automatic consolidation of multiple content blocks -- **Hot-Reload Support**: Seamless development workflow with `restart_server` - -For more information about XcodeBuildMCP's architecture and capabilities, see: -- [Architecture Guide](ARCHITECTURE.md) -- [Plugin Development Guide](PLUGIN_DEVELOPMENT.md) -- [Testing Guide](TESTING.md) diff --git a/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md b/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md deleted file mode 100644 index a2aa7e2c..00000000 --- a/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md +++ /dev/null @@ -1,302 +0,0 @@ -# Reloaderoo Usage Guide for XcodeBuildMCP - -This guide explains how to use Reloaderoo for interacting with XcodeBuildMCP as a CLI to save context window space. - -You can use this guide to prompt your agent, but providing the entire document will give you no actual benefits. You will end up using more context than just using MCP server directly. So it's recommended that you curate this document by removing the example commands that you don't need and just keeping the ones that are right for your project. You'll then want to keep this file within your project workspace and then include it in the context window when you need to interact your agent to use XcodeBuildMCP tools. - -> [!IMPORTANT] -> Please remove this introduction before you prompt your agent with this file or any derrived version of it. - -## Installation - -Reloaderoo is available via npm and can be used with npx for universal compatibility. - -```bash -# Use npx to run reloaderoo -npx reloaderoo@latest --help -``` - -**Example Tool Calls:** - -### iOS Device Development - -- **`build_device`**: Builds an app for a physical device. - ```bash - npx reloaderoo@latest inspect call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` -- **`get_device_app_path`**: Gets the `.app` bundle path for a device build. - ```bash - npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` -- **`install_app_device`**: Installs an app on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -- node build/cli.js mcp - ``` -- **`launch_app_device`**: Launches an app on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/cli.js mcp - ``` -- **`list_devices`**: Lists connected physical devices. - ```bash - npx reloaderoo@latest inspect call-tool list_devices --params '{}' -- node build/cli.js mcp - ``` -- **`stop_app_device`**: Stops an app on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -- node build/cli.js mcp - ``` -- **`test_device`**: Runs tests on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -- node build/cli.js mcp - ``` - -### iOS Simulator Development - -- **`boot_sim`**: Boots a simulator. - ```bash - npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorId": "SIMULATOR_UUID"}' -- node build/cli.js mcp - ``` -- **`build_run_sim`**: Builds and runs an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/cli.js mcp - ``` -- **`build_sim`**: Builds an app for a simulator. - ```bash - npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/cli.js mcp - ``` -- **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build. - ```bash - npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -- node build/cli.js mcp - ``` -- **`install_app_sim`**: Installs an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorId": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -- node build/cli.js mcp - ``` -- **`launch_app_logs_sim`**: Launches an app on a simulator with log capture. - ```bash - npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorId": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/cli.js mcp - ``` -- **`launch_app_sim`**: Launches an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/cli.js mcp - ``` -- **`list_sims`**: Lists available simulators. - ```bash - npx reloaderoo@latest inspect call-tool list_sims --params '{}' -- node build/cli.js mcp - ``` -- **`open_sim`**: Opens the Simulator application. - ```bash - npx reloaderoo@latest inspect call-tool open_sim --params '{}' -- node build/cli.js mcp - ``` -- **`stop_app_sim`**: Stops an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -- node build/cli.js mcp - ``` -- **`test_sim`**: Runs tests on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -- node build/cli.js mcp - ``` - -### Log Capture & Management - -- **`start_device_log_cap`**: Starts log capture for a physical device. - ```bash - npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -- node build/cli.js mcp - ``` -- **`start_sim_log_cap`**: Starts log capture for a simulator. - ```bash - npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -- node build/cli.js mcp - ``` -- **`stop_device_log_cap`**: Stops log capture for a physical device. - ```bash - npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/cli.js mcp - ``` -- **`stop_sim_log_cap`**: Stops log capture for a simulator. - ```bash - npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -- node build/cli.js mcp - ``` - -### macOS Development - -- **`build_macos`**: Builds a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` -- **`build_run_macos`**: Builds and runs a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` -- **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build. - ```bash - npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` -- **`launch_mac_app`**: Launches a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -- node build/cli.js mcp - ``` -- **`stop_mac_app`**: Stops a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -- node build/cli.js mcp - ``` -- **`test_macos`**: Runs tests for a macOS project. - ```bash - npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` - -### Project Discovery - -- **`discover_projs`**: Discovers Xcode projects and workspaces. - ```bash - npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -- node build/cli.js mcp - ``` -- **`get_app_bundle_id`**: Gets an app's bundle identifier. - ```bash - npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -- node build/cli.js mcp - ``` -- **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier. - ```bash - npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -- node build/cli.js mcp - ``` -- **`list_schemes`**: Lists schemes in a project or workspace. - ```bash - npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/cli.js mcp - ``` -- **`show_build_settings`**: Shows build settings for a scheme. - ```bash - npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` - -### Project Scaffolding - -- **`scaffold_ios_project`**: Scaffolds a new iOS project. - ```bash - npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -- node build/cli.js mcp - ``` -- **`scaffold_macos_project`**: Scaffolds a new macOS project. - ```bash - npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -- node build/cli.js mcp - ``` - -### Project Utilities - -- **`clean`**: Cleans build artifacts. - ```bash - # For a project - npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -- node build/cli.js mcp - # For a workspace - npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -- node build/cli.js mcp - ``` - -### Simulator Management - -- **`reset_sim_location`**: Resets a simulator's location. - ```bash - npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/cli.js mcp - ``` -- **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode). - ```bash - npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -- node build/cli.js mcp - ``` -- **`set_sim_location`**: Sets a simulator's GPS location. - ```bash - npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -- node build/cli.js mcp - ``` -- **`sim_statusbar`**: Overrides a simulator's status bar. - ```bash - npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -- node build/cli.js mcp - ``` - -### Swift Package Manager - -- **`swift_package_build`**: Builds a Swift package. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -- node build/cli.js mcp - ``` -- **`swift_package_clean`**: Cleans a Swift package. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -- node build/cli.js mcp - ``` -- **`swift_package_list`**: Lists running Swift package processes. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -- node build/cli.js mcp - ``` -- **`swift_package_run`**: Runs a Swift package executable. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -- node build/cli.js mcp - ``` -- **`swift_package_stop`**: Stops a running Swift package process. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -- node build/cli.js mcp - ``` -- **`swift_package_test`**: Tests a Swift package. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -- node build/cli.js mcp - ``` - -### System Doctor - -- **`doctor`**: Runs system diagnostics. - ```bash - npx reloaderoo@latest inspect call-tool doctor --params '{}' -- node build/cli.js mcp - ``` - -### UI Testing & Automation - -- **`button`**: Simulates a hardware button press. - ```bash - npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -- node build/cli.js mcp - ``` -- **`snapshot_ui`**: Gets the UI hierarchy of the current screen. - ```bash - npx reloaderoo@latest inspect call-tool snapshot_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/cli.js mcp - ``` -- **`gesture`**: Performs a pre-defined gesture. - ```bash - npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -- node build/cli.js mcp - ``` -- **`key_press`**: Simulates a key press. - ```bash - npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -- node build/cli.js mcp - ``` -- **`key_sequence`**: Simulates a sequence of key presses. - ```bash - npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -- node build/cli.js mcp - ``` -- **`long_press`**: Performs a long press at coordinates. - ```bash - npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -- node build/cli.js mcp - ``` -- **`screenshot`**: Takes a screenshot. - ```bash - npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/cli.js mcp - ``` -- **`swipe`**: Performs a swipe gesture. - ```bash - npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -- node build/cli.js mcp - ``` -- **`tap`**: Performs a tap at coordinates. - ```bash - npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -- node build/cli.js mcp - ``` -- **`touch`**: Simulates a touch down or up event. - ```bash - npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -- node build/cli.js mcp - ``` -- **`type_text`**: Types text into the focused element. - ```bash - npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -- node build/cli.js mcp - ``` - -### Resources - -- **Read devices resource**: - ```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -- node build/cli.js mcp - ``` -- **Read simulators resource**: - ```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -- node build/cli.js mcp - ``` -- **Read doctor resource**: - ```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -- node build/cli.js mcp - ``` diff --git a/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md b/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md deleted file mode 100644 index 5de23697..00000000 --- a/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md +++ /dev/null @@ -1,325 +0,0 @@ -# Reloaderoo + XcodeBuildMCP: Curated CLI Primer - -Use this primer to drive XcodeBuildMCP entirely through Reloaderoo—treating it like a CLI. It is designed to be included in your agent’s context to show exactly how to invoke the specific tools your project needs. - -Why this file: -- XcodeBuildMCP exposes many tools. Dumping the full tool surface into the context wastes tokens. -- Instead, copy this file into your project and delete everything you don’t need. Keep only the commands relevant to your workflow (e.g., just Simulator tools). -- Your trimmed version becomes a small, project‑specific reference that tells your agent precisely which Reloaderoo tool calls to make. - -How to use this primer: -1. Copy this file into your repo (e.g., docs/xcodebuildmcp_primer.md or AGENTS.md). -2. Remove all sections and commands you don’t use. Keep it minimal. -3. Replace placeholders with your real values (paths, schemes, simulator UUIDs/Names, bundle IDs, etc.). -4. Use the quiet (-q) examples to reduce noise; pipe output to jq when you only need the content. -5. Include your curated file in the agent context whenever you want it to call XcodeBuildMCP via Reloaderoo. - -Conventions in the examples: -- Calls use: npx reloaderoo@latest inspect … -q -- npx xcodebuildmcp@latest -- Parameters are passed as JSON via --params. -- Resources are read with read-resource (e.g., xcodebuildmcp://simulators). -- Use jq -r '.contents[].text' to extract the textual results when needed. - -Keep it small. The smaller your curated primer, the less context your agent needs—and the cheaper, faster, and more reliable your interactions will be. - -## Installation - -Reloaderoo is available via npm and can be used with npx for universal compatibility. - -```bash -# Use npx to run reloaderoo -npx reloaderoo@latest --help -``` - -## Hint - -Use jq to parse the output to get just the content response: - -```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -q -- npx xcodebuildmcp@latest | jq -r '.contents[].text' - ``` - -**Example Tool Calls:** - -## iOS Device Development - -- **`build_device`**: Builds an app for a physical device. - ```bash - npx reloaderoo@latest inspect -q call-tool build_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` -- **`get_device_app_path`**: Gets the `.app` bundle path for a device build. - ```bash - npx reloaderoo@latest inspect call-tool get_device_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` -- **`install_app_device`**: Installs an app on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool install_app_device --params '{"deviceId": "DEVICE_UDID", "appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest - ``` -- **`launch_app_device`**: Launches an app on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool launch_app_device --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest - ``` -- **`list_devices`**: Lists connected physical devices. - ```bash - npx reloaderoo@latest inspect call-tool list_devices --params '{}' -q -- npx xcodebuildmcp@latest - ``` -- **`stop_app_device`**: Stops an app on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool stop_app_device --params '{"deviceId": "DEVICE_UDID", "processId": 12345}' -q -- npx xcodebuildmcp@latest - ``` -- **`test_device`**: Runs tests on a physical device. - ```bash - npx reloaderoo@latest inspect call-tool test_device --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "deviceId": "DEVICE_UDID"}' -q -- npx xcodebuildmcp@latest - ``` - -## iOS Simulator Development - -- **`boot_sim`**: Boots a simulator. - ```bash - npx reloaderoo@latest inspect call-tool boot_sim --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest - ``` -- **`build_run_sim`**: Builds and runs an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool build_run_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest - ``` -- **`build_sim`**: Builds an app for a simulator. - ```bash - npx reloaderoo@latest inspect call-tool build_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest - ``` -- **`get_sim_app_path`**: Gets the `.app` bundle path for a simulator build. - ```bash - npx reloaderoo@latest inspect call-tool get_sim_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "platform": "iOS Simulator", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest - ``` -- **`install_app_sim`**: Installs an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool install_app_sim --params '{"simulatorUuid": "SIMULATOR_UUID", "appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest - ``` -- **`launch_app_logs_sim`**: Launches an app on a simulator with log capture. - ```bash - npx reloaderoo@latest inspect call-tool launch_app_logs_sim --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest - ``` -- **`launch_app_sim`**: Launches an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool launch_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest - ``` -- **`list_sims`**: Lists available simulators. - ```bash - npx reloaderoo@latest inspect call-tool list_sims --params '{}' -q -- npx xcodebuildmcp@latest - ``` -- **`open_sim`**: Opens the Simulator application. - ```bash - npx reloaderoo@latest inspect call-tool open_sim --params '{}' -q -- npx xcodebuildmcp@latest - ``` -- **`stop_app_sim`**: Stops an app on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool stop_app_sim --params '{"simulatorName": "iPhone 16", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest - ``` -- **`test_sim`**: Runs tests on a simulator. - ```bash - npx reloaderoo@latest inspect call-tool test_sim --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme", "simulatorName": "iPhone 16"}' -q -- npx xcodebuildmcp@latest - ``` - -## Log Capture & Management - -- **`start_device_log_cap`**: Starts log capture for a physical device. - ```bash - npx reloaderoo@latest inspect call-tool start_device_log_cap --params '{"deviceId": "DEVICE_UDID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest - ``` -- **`start_sim_log_cap`**: Starts log capture for a simulator. - ```bash - npx reloaderoo@latest inspect call-tool start_sim_log_cap --params '{"simulatorUuid": "SIMULATOR_UUID", "bundleId": "com.example.MyApp"}' -q -- npx xcodebuildmcp@latest - ``` -- **`stop_device_log_cap`**: Stops log capture for a physical device. - ```bash - npx reloaderoo@latest inspect call-tool stop_device_log_cap --params '{"logSessionId": "SESSION_ID"}' -q -- npx xcodebuildmcp@latest - ``` -- **`stop_sim_log_cap`**: Stops log capture for a simulator. - ```bash - npx reloaderoo@latest inspect call-tool stop_sim_log_cap --params '{"logSessionId": "SESSION_ID"}' -q -- npx xcodebuildmcp@latest - ``` - -## macOS Development - -- **`build_macos`**: Builds a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool build_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` -- **`build_run_macos`**: Builds and runs a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool build_run_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` -- **`get_mac_app_path`**: Gets the `.app` bundle path for a macOS build. - ```bash - npx reloaderoo@latest inspect call-tool get_mac_app_path --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` -- **`launch_mac_app`**: Launches a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool launch_mac_app --params '{"appPath": "/Applications/Calculator.app"}' -q -- npx xcodebuildmcp@latest - ``` -- **`stop_mac_app`**: Stops a macOS app. - ```bash - npx reloaderoo@latest inspect call-tool stop_mac_app --params '{"appName": "Calculator"}' -q -- npx xcodebuildmcp@latest - ``` -- **`test_macos`**: Runs tests for a macOS project. - ```bash - npx reloaderoo@latest inspect call-tool test_macos --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` - -## Project Discovery - -- **`discover_projs`**: Discovers Xcode projects and workspaces. - ```bash - npx reloaderoo@latest inspect call-tool discover_projs --params '{"workspaceRoot": "/path/to/workspace"}' -q -- npx xcodebuildmcp@latest - ``` -- **`get_app_bundle_id`**: Gets an app's bundle identifier. - ```bash - npx reloaderoo@latest inspect call-tool get_app_bundle_id --params '{"appPath": "/path/to/MyApp.app"}' -q -- npx xcodebuildmcp@latest - ``` -- **`get_mac_bundle_id`**: Gets a macOS app's bundle identifier. - ```bash - npx reloaderoo@latest inspect call-tool get_mac_bundle_id --params '{"appPath": "/Applications/Calculator.app"}' -q -- npx xcodebuildmcp@latest - ``` -- **`list_schemes`**: Lists schemes in a project or workspace. - ```bash - npx reloaderoo@latest inspect call-tool list_schemes --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -q -- npx xcodebuildmcp@latest - ``` -- **`show_build_settings`**: Shows build settings for a scheme. - ```bash - npx reloaderoo@latest inspect call-tool show_build_settings --params '{"projectPath": "/path/to/MyProject.xcodeproj", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` - -## Project Scaffolding - -- **`scaffold_ios_project`**: Scaffolds a new iOS project. - ```bash - npx reloaderoo@latest inspect call-tool scaffold_ios_project --params '{"projectName": "MyNewApp", "outputPath": "/path/to/projects"}' -q -- npx xcodebuildmcp@latest - ``` -- **`scaffold_macos_project`**: Scaffolds a new macOS project. - ```bash - npx reloaderoo@latest inspect call-tool scaffold_macos_project --params '{"projectName": "MyNewMacApp", "outputPath": "/path/to/projects"}' -q -- npx xcodebuildmcp@latest - ``` - -## Project Utilities - -- **`clean`**: Cleans build artifacts. - ```bash - # For a project - npx reloaderoo@latest inspect call-tool clean --params '{"projectPath": "/path/to/MyProject.xcodeproj"}' -q -- npx xcodebuildmcp@latest - # For a workspace - npx reloaderoo@latest inspect call-tool clean --params '{"workspacePath": "/path/to/MyWorkspace.xcworkspace", "scheme": "MyScheme"}' -q -- npx xcodebuildmcp@latest - ``` - -## Simulator Management - -- **`reset_sim_location`**: Resets a simulator's location. - ```bash - npx reloaderoo@latest inspect call-tool reset_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest - ``` -- **`set_sim_appearance`**: Sets a simulator's appearance (dark/light mode). - ```bash - npx reloaderoo@latest inspect call-tool set_sim_appearance --params '{"simulatorUuid": "SIMULATOR_UUID", "mode": "dark"}' -q -- npx xcodebuildmcp@latest - ``` -- **`set_sim_location`**: Sets a simulator's GPS location. - ```bash - npx reloaderoo@latest inspect call-tool set_sim_location --params '{"simulatorUuid": "SIMULATOR_UUID", "latitude": 37.7749, "longitude": -122.4194}' -q -- npx xcodebuildmcp@latest - ``` -- **`sim_statusbar`**: Overrides a simulator's status bar. - ```bash - npx reloaderoo@latest inspect call-tool sim_statusbar --params '{"simulatorUuid": "SIMULATOR_UUID", "dataNetwork": "wifi"}' -q -- npx xcodebuildmcp@latest - ``` - -## Swift Package Manager - -- **`swift_package_build`**: Builds a Swift package. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_build --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest - ``` -- **`swift_package_clean`**: Cleans a Swift package. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_clean --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest - ``` -- **`swift_package_list`**: Lists running Swift package processes. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_list --params '{}' -q -- npx xcodebuildmcp@latest - ``` -- **`swift_package_run`**: Runs a Swift package executable. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_run --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest - ``` -- **`swift_package_stop`**: Stops a running Swift package process. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_stop --params '{"pid": 12345}' -q -- npx xcodebuildmcp@latest - ``` -- **`swift_package_test`**: Tests a Swift package. - ```bash - npx reloaderoo@latest inspect call-tool swift_package_test --params '{"packagePath": "/path/to/package"}' -q -- npx xcodebuildmcp@latest - ``` - -## System Doctor - -- **`doctor`**: Runs system diagnostics. - ```bash - npx reloaderoo@latest inspect call-tool doctor --params '{}' -q -- npx xcodebuildmcp@latest - ``` - -## UI Testing & Automation - -- **`button`**: Simulates a hardware button press. - ```bash - npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -q -- npx xcodebuildmcp@latest - ``` -- **`snapshot_ui`**: Gets the UI hierarchy of the current screen. - ```bash - npx reloaderoo@latest inspect call-tool snapshot_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest - ``` -- **`gesture`**: Performs a pre-defined gesture. - ```bash - npx reloaderoo@latest inspect call-tool gesture --params '{"simulatorUuid": "SIMULATOR_UUID", "preset": "scroll-up"}' -q -- npx xcodebuildmcp@latest - ``` -- **`key_press`**: Simulates a key press. - ```bash - npx reloaderoo@latest inspect call-tool key_press --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCode": 40}' -q -- npx xcodebuildmcp@latest - ``` -- **`key_sequence`**: Simulates a sequence of key presses. - ```bash - npx reloaderoo@latest inspect call-tool key_sequence --params '{"simulatorUuid": "SIMULATOR_UUID", "keyCodes": [40, 42, 44]}' -q -- npx xcodebuildmcp@latest - ``` -- **`long_press`**: Performs a long press at coordinates. - ```bash - npx reloaderoo@latest inspect call-tool long_press --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "duration": 1500}' -q -- npx xcodebuildmcp@latest - ``` -- **`screenshot`**: Takes a screenshot. - ```bash - npx reloaderoo@latest inspect call-tool screenshot --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest - ``` -- **`swipe`**: Performs a swipe gesture. - ```bash - npx reloaderoo@latest inspect call-tool swipe --params '{"simulatorUuid": "SIMULATOR_UUID", "x1": 100, "y1": 200, "x2": 100, "y2": 400}' -q -- npx xcodebuildmcp@latest - ``` -- **`tap`**: Performs a tap at coordinates. - ```bash - npx reloaderoo@latest inspect call-tool tap --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200}' -q -- npx xcodebuildmcp@latest - ``` -- **`touch`**: Simulates a touch down or up event. - ```bash - npx reloaderoo@latest inspect call-tool touch --params '{"simulatorUuid": "SIMULATOR_UUID", "x": 100, "y": 200, "down": true}' -q -- npx xcodebuildmcp@latest - ``` -- **`type_text`**: Types text into the focused element. - ```bash - npx reloaderoo@latest inspect call-tool type_text --params '{"simulatorUuid": "SIMULATOR_UUID", "text": "Hello, World!"}' -q -- npx xcodebuildmcp@latest - ``` - -## Resources - -- **Read devices resource**: - ```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://devices" -q -- npx xcodebuildmcp@latest - ``` -- **Read simulators resource**: - ```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://simulators" -q -- npx xcodebuildmcp@latest - ``` -- **Read doctor resource**: - ```bash - npx reloaderoo@latest inspect read-resource "xcodebuildmcp://doctor" -q -- npx xcodebuildmcp@latest - ``` diff --git a/docs/dev/TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md b/docs/dev/TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md deleted file mode 100644 index e294f9e8..00000000 --- a/docs/dev/TEST_RUNNER_ENV_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,423 +0,0 @@ -# TEST_RUNNER_ Environment Variables Implementation Plan - -## Problem Statement - -**GitHub Issue**: [#101 - Support TEST_RUNNER_ prefixed env vars](https://github.com/cameroncooke/XcodeBuildMCP/issues/101) - -**Core Need**: Enable conditional test behavior by passing TEST_RUNNER_ prefixed environment variables from MCP client configurations to xcodebuild test processes. This addresses the specific use case of disabling `runsForEachTargetApplicationUIConfiguration` for faster development testing. - -## Background Context - -### xcodebuild Environment Variable Support - -From the xcodebuild man page: -``` -TEST_RUNNER_ Set an environment variable whose name is prefixed - with TEST_RUNNER_ to have that variable passed, with - its prefix stripped, to all test runner processes - launched during a test action. For example, - TEST_RUNNER_Foo=Bar xcodebuild test ... sets the - environment variable Foo=Bar in the test runner's - environment. -``` - -### User Requirements - -Users want to configure their MCP server with TEST_RUNNER_ prefixed environment variables: - -```json -{ - "mcpServers": { - "XcodeBuildMCP": { - "type": "stdio", - "command": "npx", - "args": ["-y", "xcodebuildmcp@latest"], - "env": { - "TEST_RUNNER_USE_DEV_MODE": "YES" - } - } - } -} -``` - -And have tests that can conditionally execute based on these variables: - -```swift -func testFoo() throws { - let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES" - guard useDevMode else { - XCTFail("Test requires USE_DEV_MODE to be true") - return - } - // Test logic here... -} -``` - -## Current Architecture Analysis - -### XcodeBuildMCP Execution Flow -1. All Xcode commands flow through `executeXcodeBuildCommand()` function -2. Generic `CommandExecutor` interface handles all command execution -3. Test tools exist for device/simulator/macOS platforms -4. Zod schemas provide parameter validation and type safety - -### Key Files in Current Architecture -- `src/utils/CommandExecutor.ts` - Command execution interface -- `src/utils/build-utils.ts` - Contains `executeXcodeBuildCommand` -- `src/mcp/tools/device/test_device.ts` - Device testing tool -- `src/mcp/tools/simulator/test_sim.ts` - Simulator testing tool -- `src/mcp/tools/macos/test_macos.ts` - macOS testing tool -- `src/utils/test/index.ts` - Shared test logic for simulator - -## Solution Analysis - -### Design Options Considered - -1. **Automatic Detection** (❌ Rejected) - - Scan `process.env` for TEST_RUNNER_ variables and always pass them - - **Issue**: Security risk of environment variable leakage - - **Issue**: Unpredictable behavior based on server environment - -2. **Explicit Parameter** (✅ Chosen) - - Add `testRunnerEnv` parameter to test tools - - Users explicitly specify which variables to pass - - **Benefits**: Secure, predictable, well-validated - -3. **Hybrid Approach** (🤔 Future Enhancement) - - Both automatic + explicit with explicit overriding - - **Issue**: Adds complexity, deferred for future consideration - -### Expert Analysis Summary - -**RepoPrompt Analysis**: Comprehensive architectural plan emphasizing security, type safety, and integration with existing patterns. - -**Gemini Analysis**: Confirmed explicit approach as optimal, highlighting: -- Security benefits of explicit allow-list approach -- Architectural soundness of extending CommandExecutor -- Recommendation for automatic prefix handling for better UX - -## Recommended Solution: Explicit Parameter with Automatic Prefix Handling - -### Key Design Decisions - -1. **Security-First**: Only explicitly provided variables are passed (no automatic process.env scanning) -2. **User Experience**: Automatic prefix handling - users provide unprefixed keys -3. **Architecture**: Extend execution layer generically for future extensibility -4. **Validation**: Zod schema enforcement with proper type safety - -### User Experience Design - -**Input** (what users specify): -```json -{ - "testRunnerEnv": { - "USE_DEV_MODE": "YES", - "runsForEachTargetApplicationUIConfiguration": "NO" - } -} -``` - -**Output** (what gets passed to xcodebuild): -```bash -TEST_RUNNER_USE_DEV_MODE=YES \ -TEST_RUNNER_runsForEachTargetApplicationUIConfiguration=NO \ -xcodebuild test ... -``` - -## Implementation Plan - -### Phase 0: Test-Driven Development Setup - -**Objective**: Create reproduction test to validate issue and later prove fix works - -#### Tasks: -- [ ] Create test in `example_projects/iOS/MCPTest` that checks for environment variable -- [ ] Run current test tools to demonstrate limitation (test should fail) -- [ ] Document baseline behavior - -**Test Code Example**: -```swift -func testEnvironmentVariablePassthrough() throws { - let useDevMode = ProcessInfo.processInfo.environment["USE_DEV_MODE"] == "YES" - guard useDevMode else { - XCTFail("Test requires USE_DEV_MODE=YES via TEST_RUNNER_USE_DEV_MODE") - return - } - XCTAssertTrue(true, "Environment variable successfully passed through") -} -``` - -### Phase 1: Core Infrastructure Updates - -**Objective**: Extend CommandExecutor and build utilities to support environment variables - -#### 1.1 Update CommandExecutor Interface - -**File**: `src/utils/CommandExecutor.ts` - -**Changes**: -- Add `CommandExecOptions` type for execution options -- Update `CommandExecutor` type signature to accept optional execution options - -```typescript -export type CommandExecOptions = { - cwd?: string; - env?: Record; -}; - -export type CommandExecutor = ( - args: string[], - description?: string, - quiet?: boolean, - opts?: CommandExecOptions -) => Promise; -``` - -#### 1.2 Update Execution Facade - -**File**: `src/utils/execution/index.ts` - -**Changes**: -- Re-export `CommandExecOptions` type - -```typescript -export type { CommandExecutor, CommandResponse, CommandExecOptions } from '../CommandExecutor.js'; -``` - -#### 1.3 Update Default Command Executor - -**File**: `src/utils/command.ts` - -**Changes**: -- Modify `getDefaultCommandExecutor` to merge `opts.env` with `process.env` when spawning - -```typescript -// In the returned function: -const env = { ...process.env, ...(opts?.env ?? {}) }; -// Pass env and opts?.cwd to spawn/exec call -``` - -#### 1.4 Create Environment Variable Utility - -**File**: `src/utils/environment.ts` - -**Changes**: -- Add `normalizeTestRunnerEnv` function - -```typescript -export function normalizeTestRunnerEnv( - userVars?: Record -): Record { - const result: Record = {}; - if (userVars) { - for (const [key, value] of Object.entries(userVars)) { - if (value !== undefined) { - result[`TEST_RUNNER_${key}`] = value; - } - } - } - return result; -} -``` - -#### 1.5 Update executeXcodeBuildCommand - -**File**: `src/utils/build-utils.ts` - -**Changes**: -- Add optional `execOpts?: CommandExecOptions` parameter (6th parameter) -- Pass execution options through to `CommandExecutor` calls - -```typescript -export async function executeXcodeBuildCommand( - build: { /* existing fields */ }, - runtime: { /* existing fields */ }, - preferXcodebuild = false, - action: 'build' | 'test' | 'archive' | 'analyze' | string, - executor: CommandExecutor = getDefaultCommandExecutor(), - execOpts?: CommandExecOptions, // NEW -): Promise -``` - -### Phase 2: Test Tool Integration - -**Objective**: Add `testRunnerEnv` parameter to all test tools and wire through execution - -#### 2.1 Update Device Test Tool - -**File**: `src/mcp/tools/device/test_device.ts` - -**Changes**: -- Add `testRunnerEnv` to Zod schema with validation -- Import and use `normalizeTestRunnerEnv` -- Pass execution options to `executeXcodeBuildCommand` - -**Schema Addition**: -```typescript -testRunnerEnv: z - .record(z.string(), z.string().optional()) - .optional() - .describe('Test runner environment variables (TEST_RUNNER_ prefix added automatically)') -``` - -**Usage**: -```typescript -const execEnv = normalizeTestRunnerEnv(params.testRunnerEnv); -const testResult = await executeXcodeBuildCommand( - { /* build params */ }, - { /* runtime params */ }, - params.preferXcodebuild ?? false, - 'test', - executor, - { env: execEnv } // NEW -); -``` - -#### 2.2 Update macOS Test Tool - -**File**: `src/mcp/tools/macos/test_macos.ts` - -**Changes**: Same pattern as device test tool -- Schema addition for `testRunnerEnv` -- Import `normalizeTestRunnerEnv` -- Pass execution options to `executeXcodeBuildCommand` - -#### 2.3 Update Simulator Test Tool and Logic - -**File**: `src/mcp/tools/simulator/test_sim.ts` - -**Changes**: -- Add `testRunnerEnv` to schema -- Pass through to `handleTestLogic` - -**File**: `src/utils/test/index.ts` - -**Changes**: -- Update `handleTestLogic` signature to accept `testRunnerEnv?: Record` -- Import and use `normalizeTestRunnerEnv` -- Pass execution options to `executeXcodeBuildCommand` - -### Phase 3: Testing and Validation - -**Objective**: Comprehensive testing coverage for new functionality - -#### 3.1 Unit Tests - -**File**: `src/utils/__tests__/environment.test.ts` - -**Tests**: -- Test `normalizeTestRunnerEnv` with various inputs -- Verify prefix addition -- Verify undefined filtering -- Verify empty input handling - -#### 3.2 Integration Tests - -**Files**: Update existing test files for test tools - -**Tests**: -- Verify `testRunnerEnv` parameter is properly validated -- Verify environment variables are passed through `CommandExecutor` -- Mock executor to verify correct env object construction - -#### 3.3 Tool Export Validation - -**Files**: Test files in each tool directory - -**Tests**: -- Verify schema exports include new `testRunnerEnv` field -- Verify parameter typing is correct - -### Phase 4: End-to-End Validation - -**Objective**: Prove the fix works with real xcodebuild scenarios - -#### 4.1 Reproduction Test Validation - -**Tasks**: -- Run reproduction test from Phase 0 with new `testRunnerEnv` parameter -- Verify test passes (proving env var was successfully passed) -- Document the before/after behavior - -#### 4.2 Real-World Scenario Testing - -**Tasks**: -- Test with actual iOS project using `runsForEachTargetApplicationUIConfiguration` -- Verify performance difference when variable is set -- Test with multiple environment variables -- Test edge cases (empty values, special characters) - -## Security Considerations - -### Security Benefits -- **No Environment Leakage**: Only explicit user-provided variables are passed -- **Command Injection Prevention**: Environment variables passed as separate object, not interpolated into command string -- **Input Validation**: Zod schemas prevent malformed inputs -- **Prefix Enforcement**: Only TEST_RUNNER_ prefixed variables can be set - -### Security Best Practices -- Never log environment variable values (keys only for debugging) -- Filter out undefined values to prevent accidental exposure -- Validate all user inputs through Zod schemas -- Document supported TEST_RUNNER_ variables from Apple's documentation - -## Architectural Benefits - -### Clean Integration -- Extends existing `CommandExecutor` pattern generically -- Maintains backward compatibility (all existing calls remain valid) -- Follows established Zod validation patterns -- Consistent API across all test tools - -### Future Extensibility -- `CommandExecOptions` can support additional execution options (timeout, cwd, etc.) -- Pattern can be extended to other tools that need environment variables -- Generic approach allows for non-TEST_RUNNER_ use cases in the future - -## File Modification Summary - -### New Files -- `src/utils/__tests__/environment.test.ts` - Unit tests for environment utilities - -### Modified Files -- `src/utils/CommandExecutor.ts` - Add execution options types -- `src/utils/execution/index.ts` - Re-export new types -- `src/utils/command.ts` - Update default executor to handle env -- `src/utils/environment.ts` - Add `normalizeTestRunnerEnv` utility -- `src/utils/build-utils.ts` - Update `executeXcodeBuildCommand` signature -- `src/mcp/tools/device/test_device.ts` - Add schema and integration -- `src/mcp/tools/macos/test_macos.ts` - Add schema and integration -- `src/mcp/tools/simulator/test_sim.ts` - Add schema and pass-through -- `src/utils/test/index.ts` - Update `handleTestLogic` for simulator path -- Test files for each modified tool - Add validation tests - -## Success Criteria - -1. **Functionality**: Users can pass `testRunnerEnv` parameter to test tools and have variables appear in test runner environment -2. **Security**: No unintended environment variable leakage from server process -3. **Usability**: Users specify unprefixed variable names for better UX -4. **Compatibility**: All existing test tool calls continue to work unchanged -5. **Validation**: Comprehensive test coverage proves the feature works end-to-end - -## Future Enhancements (Out of Scope) - -1. **Configuration Profiles**: Allow users to define common TEST_RUNNER_ variable sets in config files -2. **Variable Discovery**: Help users discover available TEST_RUNNER_ variables -3. **Build Tool Support**: Extend to build tools if Apple adds similar BUILD_RUNNER_ support -4. **Performance Monitoring**: Track impact of environment variable passing on build times - -## Implementation Timeline - -- **Phase 0**: 1-2 hours (reproduction test setup) -- **Phase 1**: 4-6 hours (infrastructure changes) -- **Phase 2**: 3-4 hours (tool integration) -- **Phase 3**: 4-5 hours (testing) -- **Phase 4**: 2-3 hours (validation) - -**Total Estimated Time**: 14-20 hours - -## Conclusion - -This implementation plan provides a secure, user-friendly, and architecturally sound solution for TEST_RUNNER_ environment variable support. The explicit parameter approach with automatic prefix handling balances security concerns with user experience, while the test-driven development approach ensures we can prove the solution works as intended. - -The plan leverages XcodeBuildMCP's existing patterns and provides a foundation for future environment variable needs across the tool ecosystem. \ No newline at end of file diff --git a/docs/dev/TOOL_REGISTRY_REFACTOR.md b/docs/dev/TOOL_REGISTRY_REFACTOR.md deleted file mode 100644 index b2a4a9d0..00000000 --- a/docs/dev/TOOL_REGISTRY_REFACTOR.md +++ /dev/null @@ -1,935 +0,0 @@ -# Tool Registry Refactor: Manifest-Driven Workflows & Tools (No Codegen) - -Status: Proposed (canonical target design) -Last updated: 2026-02-04 - -## Executive summary - -XcodeBuildMCP currently discovers workflows/tools using build-time filesystem scanning and code generation (`generated-plugins.ts`) and separately generates a static `tools-manifest.json` for CLI listing. This has created drift: - -- CLI `xcodebuildmcp tools` is sourced from a JSON manifest and can disagree with what the runtime CLI actually exposes (runtime visibility filtering differs). -- Tool identity can drift between filename-based naming (analysis scripts) and `export default { name: ... }` naming at runtime. - -This refactor replaces the “clever” discovery system with a **human-managed YAML manifest registry** as the **single source of truth** for: - -- Workflows and their metadata (title, description) -- Tools and their metadata (names for MCP and CLI, descriptions, routing) -- Per-runtime availability (MCP vs CLI vs daemon) -- Runtime visibility rules via **named predicates** selected in YAML - -The runtime uses the manifest to: -- Load tool implementations via dynamic import from the package `build/**` tree -- Build the CLI command tree and the `xcodebuildmcp tools` listing from the same catalog -- Register MCP tools consistently with the same filtering rules -- Support dynamic Xcode IDE tool proxying (mcpbridge) and hide conflicting XcodeBuildMCP tools when running under Xcode agent mode, per `XCODE_IDE_TOOL_CONFLICTS.md` - -Key design constraints: -- **No build-time plugin discovery/codegen** -- **Multiple YAML manifests** to avoid monolith file size -- **Predicate registry only** (YAML references predicate names; logic is coded in TS) -- Safe for `npm`/`npx` installs by resolving everything from **package root** (not `cwd`) -- Tool module metadata moves out of tool modules as part of this plan - ---- - -## Goals - -1. **Single source of truth**: one canonical registry for workflow/tool metadata and exposure rules. -2. **Consistency**: CLI `tools` output, CLI subcommands, and MCP server tool list use the same data and filtering logic. -3. **Explicit MCP vs CLI naming**: - - MCP tool name is stable and explicit. - - CLI tool name is explicit or derived from MCP name. -4. **Explicit per-runtime availability**: - - Workflows/tools can be enabled/disabled independently for MCP, CLI, and daemon. -5. **Runtime visibility is configurable and auditable**: - - Predicates are named, versioned functions in code and referenced in YAML. -6. **Dynamic Xcode IDE tools supported**: - - Proxied `xcode_tools_*` tools remain dynamic. - - When running under Xcode and Xcode Tools are active, hide conflicting XcodeBuildMCP tools as documented. - ---- - -## Non-goals - -- Rewriting tool implementations. -- Creating an expression language in YAML (predicates are registry-only). -- Supporting bundling that eliminates individual tool modules on disk. (We require that tool modules exist in the shipped package.) - ---- - -## Why a manifest registry? - -### Problems with the current approach -- Build-time codegen (`plugin-discovery.ts`) produces `generated-plugins.ts`. Runtime behavior depends on generated code correctness and build pipeline ordering. -- CLI `xcodebuildmcp tools` uses `tools-manifest.json` which bypasses runtime filtering and naming rules. -- Static analysis uses filenames for tool identity, while runtime uses exported `name`, causing mismatches. - -### Manifest registry benefits -- Human-managed, easy to reason about, explicit. -- One place to edit availability and names. -- Eliminates codegen and AST parsing complexity. -- Enables data-driven conflict filtering with Xcode IDE tools. - ---- - -## Repository layout (new) - -The registry is stored in multiple small YAML files: - -``` -manifests/ - workflows/ - simulator.yaml - device.yaml - doctor.yaml - ... - tools/ - build_sim.yaml - discover_projs.yaml - clean.yaml - ... -``` - -Rules: -- Each YAML file defines exactly one object with a unique `id`. -- Duplicate `id`s are an error. -- Workflows reference tools by tool `id`. -- Tools are defined once (no “re-export” duplication); multiple workflows can reference the same tool. - ---- - -## Canonical data model - -### Tool manifest entry - -A tool manifest entry describes: -- Where the implementation lives (module path) -- Names for MCP and CLI -- Description(s) -- Availability by runtime -- Predicates for visibility filtering -- Routing hints for daemon affinity - -Example: `manifests/tools/discover_projs.yaml` - -```yaml -id: discover_projs - -# Module identifier (extensionless). Resolved to build/.js at runtime. -# Must be package-root relative (not cwd-relative). -module: mcp/tools/project-discovery/discover_projs - -names: - # MCP name is required and must be globally unique. - mcp: discover_projs - - # CLI name optional. If omitted, derived as kebab-case of MCP name - # (e.g. discover_projs -> discover-projs) - cli: discover-projs - -description: "Discover Xcode projects/workspaces in a directory." - -availability: - mcp: true - cli: true - daemon: true - -# Visibility rules: predicate names, all must pass to expose tool. -predicates: - - hideWhenXcodeAgentMode - -routing: - stateful: false - daemonAffinity: preferred # preferred | required -``` - -### Workflow manifest entry - -Example: `manifests/workflows/simulator.yaml` - -```yaml -id: simulator -title: "iOS Simulator Development" -description: "Complete iOS development workflow targeting simulators." - -availability: - mcp: true - cli: true - daemon: true - -# Optional workflow selection rules, primarily for MCP selection defaults. -selection: - mcp: - # Mandatory workflows are always included in MCP selection. - mandatory: false - - # defaultEnabled is used when config.enabledWorkflows is empty. - defaultEnabled: true - - # autoInclude means “include when predicates pass even if not requested”. - autoInclude: false - -# Workflow-level predicates: if any fail, workflow is hidden for that runtime. -predicates: [] - -tools: - - boot_sim - - build_sim - - build_run_sim - - test_sim - - discover_projs - - clean - - list_sims -``` - -### Mandatory workflow example (session-management) - -`manifests/workflows/session-management.yaml` - -```yaml -id: session-management -title: "Session Management" -description: "Manage session defaults for project/workspace paths, scheme, simulator/device defaults." - -availability: - mcp: true - cli: false - daemon: false - -selection: - mcp: - mandatory: true - defaultEnabled: true - autoInclude: true - -tools: - - session_show_defaults - - session_set_defaults - - session_clear_defaults -``` - -### Workflow auto-inclusion example (doctor, workflow-discovery) - -`manifests/workflows/doctor.yaml` - -```yaml -id: doctor -title: "System Doctor" -description: "Diagnostics and environment checks." - -availability: { mcp: true, cli: true, daemon: true } - -selection: - mcp: - mandatory: false - defaultEnabled: false - autoInclude: true - -predicates: - - debugEnabled - -tools: - - doctor -``` - -`manifests/workflows/workflow-discovery.yaml` - -```yaml -id: workflow-discovery -title: "Workflow Discovery" -description: "Manage enabled workflows at runtime." - -availability: { mcp: true, cli: false, daemon: false } - -selection: - mcp: - mandatory: false - defaultEnabled: false - autoInclude: true - -predicates: - - experimentalWorkflowDiscoveryEnabled - -tools: - - manage_workflows -``` - ---- - -## Names and uniqueness rules - -### MCP name -- `names.mcp` is required and is the canonical identity for MCP registration. -- Must be unique across all tools. - -### CLI name -- `names.cli` optional; if omitted it is derived from MCP name: - - `_` → `-` - - camelCase → kebab-case -- Must be unique across all tools after derivation. -- If a collision occurs, the registry must explicitly set `names.cli` for one of the tools (we fail fast instead of auto-disambiguating). - -### Why this design? -- Avoids runtime ambiguity and “clever” automatic disambiguation. -- Encourages stable CLI UX. - ---- - -## Availability rules (CLI vs MCP vs daemon) - -Availability can be set at both workflow and tool level: -- Workflow availability gate applies first. -- Tool availability gate applies second. - -A tool is exposed only if: -- workflow availability for runtime is true -- tool availability for runtime is true -- all predicates (workflow + tool) pass for the current runtime context - ---- - -## Predicate registry (registry-only; YAML references predicate names) - -Predicates are named functions in code. YAML includes predicate names; there is no expression language. - -### Context passed to predicates - -```ts -type PredicateContext = { - runtime: 'cli' | 'mcp' | 'daemon'; - config: ResolvedRuntimeConfig; - - // environment-derived - runningUnderXcode: boolean; - - // dynamic bridge-derived (MCP only; false otherwise) - xcodeToolsActive: boolean; -}; -``` - -### Built-in predicates (initial set) - -- `debugEnabled`: true if config debug mode is enabled -- `experimentalWorkflowDiscoveryEnabled`: true if experimental workflow discovery is enabled -- `hideWhenXcodeAgentMode`: hides tool/workflow when running inside Xcode's coding agent. - Xcode provides native equivalents for these tools, so XcodeBuildMCP hides its versions - to avoid conflicts. This is independent of the Xcode Tools bridge (which is for external - agents connecting to Xcode's MCP server). - -This predicate powers the policy described in `XCODE_IDE_TOOL_CONFLICTS.md`. - -### Applying Xcode IDE conflict policy - -Tools that conflict with Xcode IDE tools are tagged in YAML with: - -```yaml -predicates: - - hideWhenXcodeAgentMode -``` - -This keeps the conflict policy: -- human-managed -- auditable -- easy to update without code changes - ---- - -## Dynamic Xcode IDE tools (mcpbridge) - -### What is dynamic? -When `xcode-ide` is enabled, XcodeBuildMCP proxies Xcode tools via `xcrun mcpbridge` and registers tools dynamically with names: -- `xcode_tools_` - -These tools: -- are not listed in the YAML registry (their set changes at runtime) -- are always registered under the `xcode_tools_` prefix to avoid collisions -- can trigger `tools/listChanged` updates - -### How dynamic tools influence static tool visibility -When running inside Xcode's coding agent (`runningUnderXcode`), conflict-tagged XcodeBuildMCP tools are hidden via `hideWhenXcodeAgentMode` because Xcode provides native equivalents. - -This is distinct from the Xcode Tools bridge, which allows external agents (Cursor, Claude Code, etc.) to access Xcode capabilities via its MCP server. - ---- - -## Runtime logic flows - -### CLI runtime -1. Load manifest registry from package root. -2. Build CLI exposure context: - - runtime: `cli` - - `runningUnderXcode: false` (CLI does not use Xcode IDE bridge) - - `xcodeToolsActive: false` -3. Build `ToolCatalog` from manifest (no codegen, no AST). -4. Register yargs commands from catalog. -5. `xcodebuildmcp tools` lists from the same catalog. - -### MCP runtime -1. Load manifest registry from package root. -2. Load runtime config. -3. Build exposure context: - - runtime: `mcp` - - `runningUnderXcode` from environment detection -4. Select workflows: - - include mandatory workflows - - include requested workflows - - if none requested: include defaultEnabled workflows - - include autoInclude workflows whose predicates pass -5. Initialize Xcode bridge if `xcode-ide` workflow is enabled. -6. Compute `xcodeToolsActive` from bridge status. -7. Register static tools from manifest applying availability + predicates. -8. Register dynamic `xcode_tools_*` tools from bridge. -9. On bridge status changes, recompute `xcodeToolsActive` and re-apply static tool registration. - -### Daemon runtime -1. Load manifest registry. -2. Build daemon exposure context: - - runtime: `daemon` - - runningUnderXcode=false - - xcodeToolsActive=false -3. Build daemon `ToolCatalog` from manifest and expose over daemon protocol. - ---- - -## Build and packaging requirements (npm/npx safe) - -This refactor requires that tool modules exist on disk at runtime for dynamic imports. - -### Key requirements -1. Keep a single entrypoint for users: - - CLI bin remains `build/cli.js` (or equivalent) - - MCP starts from a single JS entrypoint -2. Emit an **unbundled** build tree: - - `build/**` contains tool modules and their dependencies -3. Ship manifests: - - `manifests/**` must be included in published package -4. Resolve everything from **package root**, not `process.cwd()`: - - npx installs packages into a temporary directory; cwd can be arbitrary - - dynamic imports must use absolute paths derived from package root - -### Module resolution convention -Manifest uses extensionless module IDs: -- `module: mcp/tools/simulator/boot_sim` - -At runtime the loader imports: -- `${packageRoot}/build/${module}.js` (packaged) -- optionally `${packageRoot}/src/${module}.ts` (dev fallback) - ---- - -## Implementation plan (phased migration) - -### Phase 1: introduce manifest system + predicate registry -- Add `manifests/workflows/*.yaml` and `manifests/tools/*.yaml` -- Add manifest loader, schemas, predicate registry, exposure evaluator -- Add package-root resolver utilities -- Add tool module importer with backward-compatible adapter - -### Phase 2: fix CLI `tools` drift immediately -- Change `xcodebuildmcp tools` to list from the runtime `ToolCatalog` (manifest-based) -- Remove dependence on `tools-manifest.json` - -### Phase 3: migrate CLI catalog build to manifest-driven -- Build CLI ToolCatalog from manifest + imported tool implementations -- Enforce unique CLI names in manifest (fail fast) - -### Phase 4: migrate MCP registration to manifest-driven -- Replace generated loader usage with manifest selection + import-based registration -- Wire in Xcode bridge status → `xcodeToolsActive` updates for bridge-dependent predicates -- Apply Xcode agent conflict filtering via `hideWhenXcodeAgentMode` (based on `runningUnderXcode`) - -### Phase 5: migrate daemon to manifest-driven -- Daemon builds catalog from manifest -- CLI invocation routing unchanged, but tool list and names now consistent everywhere - -### Phase 6: move tool metadata out of tool modules -- Update tools to export only implementation (schema, handler, annotations) -- Manifest becomes authoritative for names, descriptions, routing, availability, predicates -- Remove legacy `PluginMeta`-style defaults once all tools migrated - -### Phase 7: delete legacy code paths -- Remove build-time plugin discovery + generated loaders -- Remove static AST tools analysis scripts -- Remove old manifest JSON generator -- Update docs generation to read YAML registry - ---- - -## Worked example: tagging conflict tools (Xcode agent mode) - -Per `XCODE_IDE_TOOL_CONFLICTS.md`, hide `build_sim` inside Xcode agent mode when Xcode Tools are active: - -`manifests/tools/build_sim.yaml` - -```yaml -id: build_sim -module: mcp/tools/simulator/build_sim -names: - mcp: build_sim - cli: build-sim -description: "Build an iOS app for a simulator target." -availability: { mcp: true, cli: true, daemon: true } -predicates: - - hideWhenXcodeAgentMode -routing: - stateful: false - daemonAffinity: preferred -``` - -When `runningUnderXcode=true`, `hideWhenXcodeAgentMode` fails and the tool is not registered/listed in MCP (but remains in CLI outside Xcode). This check is independent of `xcodeToolsActive` — Xcode's coding agent always provides native equivalents for these tools. - ---- - -## FAQ - -### Why not keep codegen? -Because it is a second “source of truth” and forces build pipeline coupling. The manifest removes that complexity and makes the system human-managed and predictable. - -### Will unbundled output break npx? -No, as long as: -- you ship the emitted modules and manifests -- your loader resolves from package root using `import.meta.url` and file URLs -- you do not depend on `cwd` relative paths - -### How do we prevent runtime failures due to missing modules? -Manifest loader performs startup validation: -- verifies each `module` path exists as `build/.js` in packaged mode -- fails fast with a clear error indicating which manifest entry is broken - ---- - -## Appendix: current modules to be deprecated - -- build-plugins/plugin-discovery.ts -- src/core/generated-plugins.ts -- scripts/analysis/tools-analysis.ts -- scripts/generate-tools-manifest.ts -- CLI tools listing dependence on tools-manifest.json - -These are removed once the manifest registry is the only path. - -# Plan - - - -## Proposed direction: **Yes — replace codegen + AST manifest with human-managed YAML manifests** - -Your goals map cleanly to a **data-driven manifest** architecture: - -- **One source of truth** for workflow/tool metadata: YAML. -- **No build-time plugin discovery**, no generated loaders, no AST scanning. -- **Separate MCP vs CLI names** (with sane defaults `_ ↔ -` when one is omitted). -- **Per-runtime availability** (`mcp`, `cli`, `daemon`) at both workflow and tool level. -- **Runtime visibility rules are coded predicates** from a registry and **opted-in** via manifest flags. -- **Dynamic Xcode Tools (mcpbridge) stay dynamic**, but their presence drives predicates (per `XCODE_IDE_TOOL_CONFLICTS.md`). - ---- - -## 1) Manifest system design (supports multiple manifests) - -### 1.1 Layout: many small YAML files (avoids a giant 70+ tool file) - -Use a directory with two subdirectories; each workflow/tool can be edited independently. - -``` -manifests/ - workflows/ - simulator.yaml - device.yaml - xcode-ide.yaml - ... - tools/ - build_sim.yaml - discover_projs.yaml - clean.yaml - ... -``` - -**Merge rule (simple, not clever):** -- Each file defines exactly **one object** with a unique `id`. -- Duplicate IDs across files are a **hard error** at startup / docs generation. -- No implicit overrides (keeps it easy to reason about). - -### 1.2 Canonical model: tools are defined once, workflows reference them -This avoids duplication for “re-exported” tools (e.g. `discover_projs` appearing in multiple workflows today). - -#### `manifests/tools/discover_projs.yaml` -```yaml -id: discover_projs -module: mcp/tools/project-discovery/discover_projs # extensionless -names: - mcp: discover_projs - cli: discover-projs -description: "Discover Xcode projects/workspaces in a directory." -availability: { mcp: true, cli: true, daemon: true } -predicates: - - hideWhenXcodeAgentMode # from predicate registry -routing: - stateful: false - daemonAffinity: preferred -``` - -#### `manifests/workflows/simulator.yaml` -```yaml -id: simulator -title: "iOS Simulator Development" -description: "Complete iOS development workflow targeting simulators." -availability: { mcp: true, cli: true, daemon: true } - -selection: - mcp: - mandatory: false - defaultEnabled: true # replaces hard-coded DEFAULT_WORKFLOW=simulator - autoInclude: false - -predicates: [] - -tools: - - boot_sim - - build_sim - - build_run_sim - - test_sim - - discover_projs - - clean -``` - -#### `manifests/workflows/session-management.yaml` -```yaml -id: session-management -title: "Session Management" -description: "Manage session defaults." - -availability: { mcp: true, cli: false, daemon: false } -selection: - mcp: - mandatory: true # replaces REQUIRED_WORKFLOW=session-management - defaultEnabled: true - autoInclude: true - -tools: - - session_show_defaults - - session_set_defaults - - session_clear_defaults -``` - -### 1.3 Naming rules (simple defaults) -- **MCP name**: required (`names.mcp`), stable, underscore style. -- **CLI name**: optional (`names.cli`) - - if absent: derive via `toKebabCase(names.mcp)` (underscore → hyphen) -- **Validation**: - - MCP names must be globally unique - - CLI names must be globally unique *after derivation* - - If a collision happens, manifest must specify an explicit CLI name for one of them (no automatic disambiguation). - ---- - -## 2) Predicate registry design (registry-only, referenced by name in YAML) - -### 2.1 Context object -Predicates get a runtime context; YAML only references predicate names. - -```ts -// src/visibility/predicate-types.ts -export type RuntimeKind = 'cli' | 'mcp' | 'daemon'; - -export type PredicateContext = { - runtime: RuntimeKind; - config: import('../utils/config-store.ts').ResolvedRuntimeConfig; - - // environment-derived - runningUnderXcode: boolean; - - // dynamic bridge-derived (MCP only; false elsewhere) - xcodeToolsActive: boolean; -}; -``` - -### 2.2 Registry + evaluator -```ts -// src/visibility/predicate-registry.ts -export type PredicateFn = (ctx: PredicateContext) => boolean; - -export const PREDICATES: Record = { - debugEnabled: (ctx) => ctx.config.debug, - experimentalWorkflowDiscoveryEnabled: (ctx) => ctx.config.experimentalWorkflowDiscovery, - - // Key for XCODE_IDE_TOOL_CONFLICTS.md — hides tools when running inside Xcode's - // coding agent, where Xcode provides native equivalents. - hideWhenXcodeAgentMode: (ctx) => !ctx.runningUnderXcode, -}; - -export function evalPredicates(names: string[] | undefined, ctx: PredicateContext): boolean { - for (const name of names ?? []) { - const fn = PREDICATES[name]; - if (!fn) throw new Error(`Unknown predicate '${name}'`); - if (!fn(ctx)) return false; - } - return true; -} -``` - -### 2.3 Applying `XCODE_IDE_TOOL_CONFLICTS.md` -Instead of hardcoding conflict tool lists in TS, you mark the conflicting tools in YAML with: - -```yaml -predicates: - - hideWhenXcodeAgentMode -``` - -That is the clean “single source of truth” you’re looking for. - ---- - -## 3) Runtime loaders (no codegen): manifest → tool modules → catalogs/registration - -### 3.1 Manifest loader (multi-file) -New module reads all YAML files, validates structure, and produces an in-memory `ResolvedManifest`. - -```ts -// src/core/manifest/load-manifest.ts -export type ToolManifestEntry = { - id: string; - module: string; // extensionless module path - names: { mcp: string; cli?: string }; - description?: string; - availability: { mcp: boolean; cli: boolean; daemon: boolean }; - predicates?: string[]; - routing?: { stateful?: boolean; daemonAffinity?: 'preferred' | 'required' }; -}; - -export type WorkflowManifestEntry = { - id: string; - title: string; - description: string; - availability: { mcp: boolean; cli: boolean; daemon: boolean }; - selection?: { mcp?: { mandatory?: boolean; defaultEnabled?: boolean; autoInclude?: boolean } }; - predicates?: string[]; - tools: string[]; // tool IDs -}; - -export type ResolvedManifest = { - tools: Map; - workflows: Map; -}; -``` - -**Key design choice:** module path is **extensionless** and package-relative (e.g. `mcp/tools/simulator/boot_sim`). Loader resolves `.js` in built output (and optionally `.ts` in dev). - -```ts -export async function importToolModule(moduleId: string): Promise<{ - schema: import('../plugin-types.ts').ToolSchemaShape; - handler: import('../plugin-types.ts').PluginMeta['handler']; - annotations?: import('@modelcontextprotocol/sdk/types.js').ToolAnnotations; -}> { /* adapter supports default export or named exports */ } -``` - ---- - -## 4) Unified filtering/exposure rules (MCP + CLI + daemon) - -### 4.1 Single filter function (manifest + predicates + runtime) -```ts -// src/visibility/exposure.ts -export function isWorkflowEnabledForRuntime( - wf: WorkflowManifestEntry, - ctx: PredicateContext, -): boolean; - -export function isToolExposedForRuntime( - tool: ToolManifestEntry, - wf: WorkflowManifestEntry, - ctx: PredicateContext, -): boolean; -``` - -Logic: -- availability gate: `wf.availability[ctx.runtime]` and `tool.availability[ctx.runtime]` -- predicate gate: `evalPredicates(wf.predicates, ctx)` and `evalPredicates(tool.predicates, ctx)` - -This replaces: -- `src/utils/tool-visibility.ts` (hardcoded xcode-ide debug tools) -- the CLI manifest JSON listing divergence -- the plugin discovery / generated loaders path - ---- - -## 5) MCP workflow selection becomes data-driven - -Replace the hard-coded constants in `src/utils/workflow-selection.ts` with manifest `selection.mcp`. - -**Selection rule (simple and matches current behavior):** -1. Start with `mandatory: true` -2. Add `autoInclude: true` if its predicates pass (covers `doctor` when debug enabled, `workflow-discovery` when experimental enabled) -3. If user config `enabledWorkflows` is empty: - - include workflows with `defaultEnabled: true` -4. Else include requested workflows -5. Finally filter by availability + predicates - -This gives you the same behavior you currently have, but **declared in YAML**. - ---- - -## 6) Dynamic Xcode Tools (mcpbridge) integration (kept, but influences predicates) - -### 6.1 Determine `xcodeToolsActive` -In MCP bootstrap, set `ctx.xcodeToolsActive` based on bridge status: - -- `workflowEnabled && bridgeAvailable && connected && proxiedToolCount > 0` - -### 6.2 Re-apply static tool registration when bridge becomes active/inactive -When the bridge syncs tools / disconnects, `xcodeToolsActive` can flip. If it flips, re-run the static registration pass so predicates that depend on `xcodeToolsActive` (e.g. `requiresXcodeTools`) take effect immediately. - -Note: `hideWhenXcodeAgentMode` depends only on `runningUnderXcode`, not bridge status — Xcode's coding agent always provides native equivalents regardless of bridge state. - ---- - -# Implementation plan (concrete file-by-file changes) - -## Phase 1 — Add manifest system + predicate registry (no behavior change yet) - -### New files -- `manifests/workflows/*.yaml` -- `manifests/tools/*.yaml` -- `src/core/manifest/schema.ts` (zod schemas for both yaml types) -- `src/core/manifest/load-manifest.ts` (loads and merges files) -- `src/core/manifest/import-tool-module.ts` (extension resolution + adapter) -- `src/visibility/predicate-types.ts` -- `src/visibility/predicate-registry.ts` -- `src/visibility/exposure.ts` - -### Build/config update -You’ll need to ensure `manifests/**` is included in the published/built artifact. -- Likely update `package.json` `files` field and/or build copy step. -- If you’re currently bundling, switch to emitting a directory structure (so dynamic imports work). - -**Side effects:** none yet; just plumbing. - ---- - -## Phase 2 — Make CLI `tools` list come from runtime catalog (eliminate JSON manifest path) - -### Modify -- `src/cli/commands/tools.ts` - - Change `registerToolsCommand(app)` → `registerToolsCommand(app, catalog)` - - Remove all `tools-manifest.json` reading logic - - List from `catalog.tools` - -- `src/cli/yargs-app.ts` - - `registerToolsCommand(app);` → `registerToolsCommand(app, opts.catalog);` - -**Result:** CLI `tools` can no longer drift from actual CLI-exposed tools. - ---- - -## Phase 3 — Replace runtime `ToolCatalog` builder with manifest-driven builder - -### Modify / replace -- `src/runtime/tool-catalog.ts` - - New entrypoint: - ```ts - export async function buildToolCatalogFromManifest(opts: { - runtime: 'cli' | 'daemon'; - manifest: ResolvedManifest; - ctx: PredicateContext; - includeWorkflows?: string[]; - excludeWorkflows?: string[]; - }): Promise - ``` - - Build `ToolDefinition` from manifest tool entries + imported module `{schema, handler, annotations}` - - Apply exposure via `isToolExposedForRuntime(...)` - - Validate CLI/MCP name uniqueness at construction time (hard error) - -### Modify -- `src/cli/cli-tool-catalog.ts` - - Replace `listWorkflowDirectoryNames()` + `buildToolCatalog(...)` with: - - `loadManifest()` - - build context for CLI (`runningUnderXcode=false`, `xcodeToolsActive=false`) - - `buildToolCatalogFromManifest({ runtime: 'cli', ... })` - -### Modify -- `src/daemon.ts` - - Build catalog using manifest with `runtime: 'daemon'` - -**Side effect:** tool exposure now fully controlled by YAML + predicate registry in CLI/daemon. - ---- - -## Phase 4 — MCP registration becomes manifest-driven - -### Modify -- `src/utils/tool-registry.ts` - - Stop calling `loadWorkflowGroups()` (plugin registry) - - Instead: - - load manifest - - compute selected workflows using new manifest-driven workflow selection - - for each exposed tool, import module + `server.registerTool(mcpName, ...)` - -### Modify -- `src/server/bootstrap.ts` - - Build `PredicateContext` for MCP: - - `runningUnderXcode = detectRunningUnderXcode()` - - `xcodeToolsActive = computed from bridge status` - - Ensure bridge is initialized *before* final static tool registration if you want conflict hiding to apply on first list. - -### Modify -- `src/integrations/xcode-tools-bridge/manager.ts` - - Add status-change callback/event so bootstrap can re-apply registration when bridge status changes. - ---- - -## Phase 5 — Remove codegen + generated-plugin pipeline - -### Remove/stop using -- `build-plugins/plugin-discovery.ts` -- `src/core/generated-plugins.ts` -- `src/core/plugin-registry.ts` (either delete or repurpose to be a manifest registry) -- scripts: - - `scripts/analysis/tools-analysis.ts` - - `scripts/generate-tools-manifest.ts` - -### Update any imports -- Replace `WORKFLOW_METADATA` uses (CLI help) with manifest workflow titles/descriptions. - ---- - -## Phase 6 — Move tool metadata out of tool modules (part of this plan) - -### Target tool module shape -Instead of `export default { name, description, schema, handler, cli... }`, tools should export **only implementation**: - -```ts -// src/mcp/tools/simulator/boot_sim.ts -export const schema = baseSchemaObject.shape; -export const annotations = { /* optional */ }; -export async function handler(params: Record) { ... } -``` - -Your manifest provides: -- names (mcp/cli) -- descriptions (cli/mcp if you want separate) -- routing (stateful/daemonAffinity) -- availability -- predicates - -### Adapter during migration -`import-tool-module.ts` should support both forms initially: -- If `default` export exists and looks like old `PluginMeta`, take `schema/handler/annotations` -- Else use named exports - -Once migrated, you can delete `PluginMeta`-style exports and simplify types. - -**Side effects / important note:** any tool code that emits `nextSteps` referencing tool names must match manifest MCP names. This is easiest if MCP names remain the underscore-stable names you already use (recommended). - ---- - -## Notes - -### (1) Multiple manifests? -Yes—designed in. Use `manifests/tools/*.yaml` and `manifests/workflows/*.yaml`. The loader merges them with duplicate-ID errors. This keeps each file small and easy to manage. - -### (2) Move meta out of tool modules as part of the plan? -Included as **Phase 6** with a supported migration adapter so you can convert incrementally but still land “manifest is authoritative” early. - -### (3) Predicates registry only? -Yes—YAML only contains predicate names. Unknown predicate names are a startup error. - diff --git a/docs/dev/XCODE_IDE_CLI_DYNAMIC_TOOLS_ARCHITECTURE.md b/docs/dev/XCODE_IDE_CLI_DYNAMIC_TOOLS_ARCHITECTURE.md deleted file mode 100644 index 2381ffc6..00000000 --- a/docs/dev/XCODE_IDE_CLI_DYNAMIC_TOOLS_ARCHITECTURE.md +++ /dev/null @@ -1,213 +0,0 @@ -# Xcode IDE Dynamic Tools in CLI: Plan and Architecture - -## Problem statement - -Today, `xcode-ide` dynamic tools can work in MCP mode, but CLI mode has sharp edges: - -- Each CLI invocation is a new process, so bridge connections are short-lived and repeated. -- Repeated bridge connects trigger repeated Xcode allow prompts. -- `--help` and exploratory commands can create extra connection churn. -- Static bridge tools and dynamic bridge-derived tools have different gating rules, but this is easy to misinterpret. - -The immediate user-visible outcome is: CLI appears inconsistent even when Xcode and bridge are available. - -## Goals - -- Keep MCP mode unchanged: no daemon requirement for MCP server flow. -- Make CLI dynamic xcode-ide tools reliable and predictable. -- Minimize repeated Xcode allow prompts in CLI workflows. -- Separate concerns so transport/client logic is reused across runtimes. -- Keep manifest-driven visibility semantics explicit and testable. - -## Non-goals - -- Replacing `xcrun mcpbridge`. -- Rewriting tool manifests or predicate system. -- Forcing daemon usage for all CLI tools. -- Reintroducing generic daemon affinity knobs for CLI routing. - -## Current behavior to preserve - -- Workflow/tool visibility remains manifest-driven. -- `availability.mcp: true, availability.cli: false` means MCP-only exposure. -- `availability.mcp: true, availability.cli: true` means exposed in both MCP and CLI. -- `xcode-ide` has: - - Static bridge tools (debug-gated). - - Dynamic tools resolved from `mcpbridge` `tools/list` (not debug-gated). - -## Why CLI currently behaves differently - -CLI is a process-per-command model. If each command independently connects to `mcpbridge`, then: - -- the bridge handshake repeats, -- Xcode trust prompts can repeat, -- command latency increases, -- and help/discovery flows can feel flaky. - -MCP mode avoids this by being long-running. - -## Clean architecture (least repetition) - -Use two layers plus runtime adapters: - -### Layer 1: Bridge client core (shared, runtime-agnostic) - -Responsibilities: - -- Discover bridge availability (`xcrun --find mcpbridge`). -- Open/close MCP client transport to `mcpbridge`. -- Call `tools/list`, `tools/call`. -- Surface normalized errors and bridge state events. - -Output: - -- `BridgeCapabilities` (available, connected, tool count). -- `BridgeToolCatalog` (dynamic tool metadata). -- `BridgeInvoker` (`invoke(name, args)`). - -This layer contains no CLI command wiring and no MCP server registration logic. - -### Layer 2: Xcode IDE tool service (shared domain layer) - -Responsibilities: - -- Build runtime-facing tool catalog from: - - static manifest-defined bridge tools, - - dynamic bridge-derived tools. -- Apply visibility and predicate checks. -- Expose `listTools(runtimeContext)` and `invokeTool(toolName, args)`. - -This is the single source of truth for xcode-ide tool behavior. - -### Adapters - -- MCP adapter: - - Binds service into long-running server lifecycle. - - Connect once at startup (if enabled), resync on bridge events. - - No daemon required. - -- CLI adapter: - - Uses same service API. - - Default path should be daemon-backed for xcode-ide dynamic tools. - -## CLI daemon strategy for xcode-ide only - -To remove repeated prompts, CLI should reuse one bridge session across commands. - -Recommended approach: - -- Introduce an xcode-ide bridge session in existing daemon runtime. -- Keep this as an explicit xcode-ide special-case bridge path (not generic affinity routing). -- CLI xcode-ide commands route to daemon when dynamic bridge tools are involved. -- Keep non-xcode-ide CLI commands unchanged. - -Lifecycle: - -1. CLI command asks daemon for xcode-ide tool catalog. -2. Daemon ensures single bridge session exists. -3. Daemon returns catalog or executes tool call over the same session. -4. Session remains warm for subsequent CLI commands. -5. Daemon exits automatically after idle timeout when no active stateful sessions remain. - -This preserves MCP independence while making CLI behavior consistent. - -## Visibility and registration rules - -Apply these rules explicitly: - -- Workflow appears in MCP/CLI when `workflow.availability[runtime]` is true. -- Daemon execution backend is not controlled by `availability` flags. -- Static tools appear only when: - - tool availability passes for MCP/CLI runtime, - - predicates pass, - - debug gate passes (for debug-only static tools). -- Dynamic tools appear only when: - - workflow is enabled for runtime, - - bridge is available and connected, - - `tools/list` returns entries. -- Dynamic tools are never controlled by static debug gates. - -## CLI UX behavior - -Expected behavior for `node build/cli.js xcode-ide ...`: - -- If workflow disabled for CLI: - - show actionable message explaining manifest controls this. -- If workflow enabled but Xcode bridge unavailable: - - show actionable setup guidance: - - open Xcode, - - enable `Settings > Intelligence > Xcode Tools`, - - accept allow prompt. -- If daemon session unavailable: - - auto-start daemon or print exact daemon start command (project decision). -- If bridge connected: - - dynamic tools are listed and invokable. - -## Implementation plan - -### Phase 1: Extract shared bridge client core - -- Isolate bridge transport/discovery/invocation into a runtime-agnostic module. -- Add typed status model and normalized error mapping. - -### Phase 2: Build shared xcode-ide tool service - -- Centralize static + dynamic catalog assembly. -- Centralize runtime visibility checks. -- Centralize invocation dispatch. - -### Phase 3: Rewire MCP adapter - -- Use shared service for registration and calls. -- Keep current long-running behavior. -- Ensure no daemon dependency in MCP code path. - -### Phase 4: Add CLI daemon-backed bridge path - -- Route xcode-ide dynamic tool list/call through daemon session. - -### Phase 5: Harden messages and observability - -- Standardize actionable operator messages for disabled/unavailable/not-authorized states. -- Log bridge session reuse metrics and reconnect causes. - -### Phase 6: Validate with manual tests - -Run manual tests across these scenarios: - -- MCP mode, workflow enabled, Xcode available. -- MCP mode, workflow enabled, Xcode unavailable. -- CLI mode, workflow disabled. -- CLI mode, workflow enabled, first-time allow prompt. -- CLI mode, workflow enabled, repeated commands without extra prompts. -- CLI mode, `--help` usage does not force repeated authorization churn. - -## Risks - -- Daemon session ownership bugs can create reconnect loops and prompt spam. -- Stale bridge session state can hide tools unexpectedly. -- Mixed one-shot/daemon modes can drift without a single service contract. - -Mitigations: - -- Single authoritative service interface for list/call. -- Explicit reconnect backoff and no tight retry loops. -- Structured logs with correlation IDs for CLI command -> daemon request -> bridge call. - -## Acceptance criteria - -- MCP mode unchanged and independent from daemon. -- CLI xcode-ide dynamic tools work when workflow `cli: true`. -- Repeated CLI xcode-ide commands do not trigger repeated allow prompts under normal operation. -- Static debug-gated tools remain debug-gated only. -- Dynamic tools appear based on bridge health, not debug gating. - -## Decision summary - -The cleanest architecture is to separate: - -- bridge transport/client core, -- xcode-ide tool service, -- runtime adapters (MCP and CLI). - -Then route CLI xcode-ide dynamic tools through a persistent daemon-held bridge session while keeping MCP flow long-running and daemon-free. diff --git a/docs/dev/XCODE_IDE_MCPBRIDGE_PLAN.md b/docs/dev/XCODE_IDE_MCPBRIDGE_PLAN.md deleted file mode 100644 index 67c27e38..00000000 --- a/docs/dev/XCODE_IDE_MCPBRIDGE_PLAN.md +++ /dev/null @@ -1,239 +0,0 @@ -# Xcode IDE MCP Bridge Plan (Xcode 26.x `xcrun mcpbridge`) - -This document is an implementation plan to extend XcodeBuildMCP by optionally proxying Apple Xcode’s “Xcode Tools” MCP service through `xcrun mcpbridge`. - -## Goal - -Add an **optional** workflow to XcodeBuildMCP that: - -- Detects whether `xcrun mcpbridge` is available. -- If available and enabled, starts a long-lived `mcpbridge` child process. -- Connects to it as an **MCP client** over stdio. -- Discovers Xcode’s advertised tools (`tools/list`) and listens for tool list changes (`tools/listChanged`). -- Dynamically registers proxied tools in XcodeBuildMCP and forwards calls to Xcode through the bridge. -- Handles tool catalog changes over time without breaking XcodeBuildMCP. - -Non-goals: - -- Reverse engineering Xcode’s internal XPC transport (we rely on Apple’s `mcpbridge` for that). -- Keeping a stable, curated wrapper API for Xcode tools (we proxy dynamically; stability is best-effort). - -## Why This Is Worth Doing (Capabilities We Gain) - -XcodeBuildMCP is currently strongest at `xcodebuild` + simulator/device workflows. The Xcode IDE MCP service provides **IDE-only** capabilities that `xcodebuild` cannot: - -- SwiftUI Preview rendering to an image (`RenderPreview`). -- Issue Navigator issues (including workspace/package resolution problems) (`XcodeListNavigatorIssues`). -- “Refresh code issues for file” style diagnostics (`XcodeRefreshCodeIssuesInFile`). -- Apple Developer Documentation semantic search (`DocumentationSearch`). -- Execute code snippets in file context (`ExecuteSnippet`). -- Project navigator operations on **Xcode project structure**, not raw filesystem (`XcodeLS`, `XcodeGlob`, `XcodeGrep`, `XcodeRead`, `XcodeWrite`, `XcodeUpdate`, `XcodeMV`, `XcodeRM`, `XcodeMakeDir`). -- IDE-scoped build/test orchestration and richer result structures (`BuildProject`, `GetBuildLog`, `GetTestList`, `RunAllTests`, `RunSomeTests`). -- Xcode window/tab discovery to obtain `tabIdentifier` (`XcodeListWindows`). - -This complements XcodeBuildMCP rather than replacing it. - -## Observed Bridge Behavior (Local Probing) - -- `xcrun --find mcpbridge` resolves to Xcode’s bundled binary: - - `/Applications/Xcode.app/Contents/Developer/usr/bin/mcpbridge` -- The bridge is a stdio JSON-RPC server that speaks MCP. -- Initialization works with MCP protocol version `2024-11-05` and server reports: - - `serverInfo.name = "xcode-tools"` - - `capabilities.tools.listChanged = true` -- `tools/call` works (e.g. calling `XcodeListWindows` returned a `tabIdentifier` and workspace path). -- Xcode may show “trust/allow client prompts” when the bridge connects and/or when tools are invoked. This plan avoids noisy retry loops to reduce prompt spam. - -## Proposed Architecture - -```mermaid -flowchart LR - A["MCP client (Codex/Claude/etc)"] -->|tools/call| B["XcodeBuildMCP server"] - B -->|stdio MCP client| C["xcrun mcpbridge"] - C -->|private endpoint injection + XPC| D["Xcode IDE tool service"] -``` - -Key choice: **XcodeBuildMCP remains the server**. When the optional workflow is enabled, it embeds an internal MCP client to Xcode’s tool service via `mcpbridge`. - -## Tool Naming / Collision Strategy - -Expose proxied tools with a clear prefix: - -- Local tool name: `xcode_tools_` -- Example: `xcode_tools_XcodeListWindows` - -Rationale: - -- Avoid collisions with existing XcodeBuildMCP tools. -- Make provenance explicit for agents. - -## Safety Policy - -Proxy **all tools** that Xcode advertises (including mutating tools like `XcodeWrite`, `XcodeUpdate`, `XcodeRM`), as requested. - -Mitigations: - -- Keep the prefix (`xcode_tools_`) so call sites can easily reason about what they’re invoking. -- Add an `annotations.readOnlyHint` best-effort heuristic (mutators get `false`). -- Add clear documentation that these are IDE-scoped filesystem/project mutations. - -## Workflow / Enablement - -Add a new workflow group: - -- Directory: `src/mcp/tools/xcode-ide/` -- Workflow name: `xcode-ide` - -Enablement is via existing workflow selection (`enabledWorkflows`), e.g.: - -```yaml -schemaVersion: 1 -enabledWorkflows: ["simulator", "debugging", "xcode-ide"] -``` - -Behavior: - -- If `xcode-ide` workflow is not enabled, no bridge code runs. -- If enabled but `mcpbridge` is missing/unusable, we register **only** small “bridge management” tools (status + manual sync) and no proxied tools. -- If enabled and bridge is healthy, we dynamically register proxied tools based on `tools/list`. - -## Internal Modules To Add - -New directory: - -- `src/integrations/xcode-tools-bridge/` - -### `client.ts` - -Responsibilities: - -- Spawn `xcrun mcpbridge` using `StdioClientTransport` (`@modelcontextprotocol/sdk` client). -- Provide `connectOnce()`, `disconnect()`, `listTools()`, and `callTool(name,args)`. -- Apply request timeouts (avoid hanging forever when Xcode prompts). -- Listen for `tools/listChanged` notifications and trigger a re-sync. - -Environment plumbing: - -- Optional `XCODEBUILDMCP_XCODE_PID` -> `MCP_XCODE_PID` -- Optional `XCODEBUILDMCP_XCODE_SESSION_ID` -> `MCP_XCODE_SESSION_ID` - -### `jsonschema-to-zod.ts` - -Convert Xcode tool JSON Schemas to Zod shapes for XcodeBuildMCP’s `server.registerTool`. - -Support a pragmatic subset (covers the schema you provided): - -- `type: object` with `properties`, `required` -- primitives: `string`, `integer`, `number`, `boolean` -- `enum` -- `type: array` with `items` - -Forward-compat strategy: - -- Unknown constructs fall back to `z.any()` for that property. -- Top-level object schemas are `passthrough()` to tolerate added fields without breaking. - -### `registry.ts` - -Responsibilities: - -- Maintain a mapping of remote tool -> registered local tool. -- Given latest `tools/list`: - - Register new tools. - - Update tools when schema/metadata changes (remove + re-register). - - Remove tools no longer present. -- Proxy handler behavior: - - `xcode_tools_` simply forwards to `tools/call` on the bridge client. - -## MCP Tooling Added by XcodeBuildMCP (Bridge Management) - -These are non-proxied, always-stable tools in the `xcode-ide` workflow: - -- `xcode_tools_bridge_status` - - Returns availability/health: - - whether `xcrun` can find `mcpbridge` - - whether Xcode is running (optional hint) - - connection status, last error, number of proxied tools currently registered - - current pid/session id used (if any) -- `xcode_tools_bridge_sync` - - One-shot connect + tool sync attempt (manual retry; avoids background prompt spam) -- Optional: `xcode_tools_bridge_disconnect` - - Stop bridge and unregister proxied tools - -## Bootstrap / Lifecycle Integration - -Entry point: - -- `src/server/bootstrap.ts` after `registerWorkflows(enabledWorkflows)`: - - If `xcode-ide` is enabled, start bridge `connectOnce()` and attempt tool sync. - -Shutdown: - -- `src/server/start-mcp-server.ts` SIGINT/SIGTERM handlers: - - Disconnect bridge cleanly before closing server. - -Failure modes: - -- Bridge process exits unexpectedly: - - Unregister proxied tools. - - Status tool reports last error. - - Manual sync tool can be used to retry. - -## Handling Tool Catalog Changes Over Time - -- Use `tools/list` as source of truth on connect. -- Subscribe to list-changed (`tools.listChanged = true` is advertised by server) and re-sync on change. -- Do not assume tool names are stable; tool proxy registry is dynamic. -- Zod schema conversion is best-effort and permissive (`passthrough`, `z.any()` fallback) to avoid breaking when Apple extends schemas. - -## Testing Strategy - -### Unit tests (Vitest) - -- JSON Schema -> Zod conversion: - - object/required/enum/array nesting cases based on the provided schema. - - unknown schema constructs degrade to `z.any()`. - -### Integration tests (no Xcode required) - -- Fake MCP server over stdio that: - - supports `initialize`, `tools/list`, `tools/call` - - emits a `tools/listChanged` notification -- Validate: - - proxied tools register with correct `xcode_tools_` prefix - - tool calls forward correctly - - list-changed causes add/remove/re-register - -### Manual “real Xcode” probe (repeatable) - -Add a script: - -- `scripts/probe-xcode-mcpbridge.ts` - - connects to `xcrun mcpbridge` - - prints server info + tool count + first N tool names - - calls `XcodeListWindows` and prints discovered `tabIdentifier`s - -This should be used during dev because Xcode prompts are human-mediated and not CI-friendly. - -## Documentation Updates - -- New doc: - - `docs/XCODE_IDE_MCPBRIDGE.md` (user-facing-ish) - - how to enable `xcode-ide` - - how `tabIdentifier` works - - how to troubleshoot trust prompts and missing tools -- Update: - - `docs/TOOLS.md` to include `xcode-ide` workflow and stable bridge tools - - Document that proxied tools are dynamic and appear as `xcode_tools_*`. - -## Acceptance Criteria - -- With `xcode-ide` enabled and Xcode running: - - XcodeBuildMCP registers `xcode_tools_` for all tools returned by `tools/list`. - - Calling a proxied tool forwards to Xcode and returns the result content unchanged. -- With `xcode-ide` enabled and no bridge available: - - Only `xcode_tools_bridge_status` and `xcode_tools_bridge_sync` are present. - - Server startup remains healthy. -- If Xcode updates the tool catalog while running: - - proxied tool set updates without server restart. - diff --git a/docs/dev/XCODE_IDE_TOOL_CONFLICTS.md b/docs/dev/XCODE_IDE_TOOL_CONFLICTS.md deleted file mode 100644 index 8ffbd8d9..00000000 --- a/docs/dev/XCODE_IDE_TOOL_CONFLICTS.md +++ /dev/null @@ -1,328 +0,0 @@ -# Tool Conflicts: XcodeBuildMCP vs Xcode Tools MCP (running inside Xcode agent) - -Date: 2026-02-04 - -This report is scoped to the “user is running inside Xcode’s integrated coding agent” scenario (Codex/Claude-in-Xcode), where the user expectation is that **build/test operations happen inside the Xcode IDE**, not via external `xcodebuild` invocations. - -The goal is to identify which existing XcodeBuildMCP tools become redundant or counterproductive once we also expose Xcode’s own MCP toolset via `xcrun mcpbridge` (proxied as `xcode_tools_*`). - -## Inputs / assumptions - -- XcodeBuildMCP can detect it is running under Xcode via process-tree detection (`detectXcodeRuntime` / `runningUnderXcode`). -- When `xcode-ide` workflow is enabled and the bridge is healthy, Xcode’s “Xcode Tools” MCP server tools are proxied and exposed as `xcode_tools_` (e.g. `xcode_tools_BuildProject`). -- The Xcode Tools MCP service includes IDE-scoped tools such as: - - `BuildProject`, `GetBuildLog` - - `GetTestList`, `RunAllTests`, `RunSomeTests` - - `XcodeListWindows` (to obtain `tabIdentifier`) - - IDE diagnostics + previews (`XcodeListNavigatorIssues`, `XcodeRefreshCodeIssuesInFile`, `RenderPreview`) - - Project navigator operations (`XcodeLS`, `XcodeRead`, `XcodeWrite`, `XcodeUpdate`, `XcodeMV`, `XcodeRM`, `XcodeMakeDir`, `XcodeGlob`, `XcodeGrep`) -- This report intentionally keeps the policy simple: tools marked “Yes” in the table are hidden whenever **(1)** we are running under Xcode and **(2)** Xcode Tools MCP is enabled/available. We do not try to detect project type, and we do not gate on the presence of individual remote tool names. - -This report focuses on **behavioral conflicts** (user intent + Xcode expectation), not literal name collisions (we prefix proxied tools so there are no name collisions). - -## What “conflict” means in the Xcode agent context - -When inside Xcode: - -- Running `xcodebuild` externally is surprising: users expect Xcode’s scheme/configuration, IDE build settings, and build system state to be authoritative. -- External builds/tests risk divergence from the IDE state (indexing, derived data location, active scheme/config, workspace selection, build logs, test results). -- The Xcode Tools MCP tools are **IDE-scoped**: they naturally align with “build/test in the IDE”. - -Therefore, any XcodeBuildMCP tool that primarily exists to build/test a workspace/project should be considered a “conflict” when Xcode Tools MCP is available. - -## Recommended mapping (Xcode Tools MCP → XcodeBuildMCP tools to hide) - -This table is the actionable “what to hide” list for the Xcode agent scenario. - -| User intent inside Xcode | Prefer Xcode Tools MCP | Hide XcodeBuildMCP tools | Notes | -| --- | --- | --- | --- | -| Build (iOS simulator) | `xcode_tools_BuildProject` | `simulator/build_sim`, `simulator/build_run_sim` | Avoid external `xcodebuild` builds competing with the IDE. | -| Build (iOS device) | `xcode_tools_BuildProject` | `device/build_device` | Same reasoning as simulator; keep device install/launch tools. | -| Build (macOS) | `xcode_tools_BuildProject` | `macos/build_macos`, `macos/build_run_macos` | Keep `launch_mac_app`/`stop_mac_app` if they remain useful. | -| Run tests | `xcode_tools_RunAllTests` / `xcode_tools_RunSomeTests` | `simulator/test_sim`, `device/test_device`, `macos/test_macos` | Use `xcode_tools_GetTestList` for discovery. | -| Select active workspace/tab | `xcode_tools_XcodeListWindows` | `*/discover_projs` | When inside Xcode, prefer tab-scoped operations over filesystem scanning. | -| SwiftPM build/test (inside Xcode) | `xcode_tools_BuildProject` / `xcode_tools_RunAllTests` | `swift-package/swift_package_build`, `swift-package/swift_package_test` | We likely cannot reliably detect “SPM-only vs Xcode-opened”; treat “inside Xcode” as sufficient. | -| Clean build artifacts | (TBD: if Xcode exposes clean) | `*/clean` | External cleans are surprising in IDE mode; hide unless we explicitly want them as an escape hatch. | -| Scaffold new projects | (none) | `project-scaffolding/*` | Inside Xcode agent mode the user already has an open workspace; scaffolding is noise by default. | -| Inspect build/test results | `xcode_tools_GetBuildLog` | (none) | We don’t have an equivalent; this is additive. | -| Inspect IDE issues/diagnostics | `xcode_tools_XcodeListNavigatorIssues`, `xcode_tools_XcodeRefreshCodeIssuesInFile` | (none) | Additive; these are uniquely IDE-scoped. | -| Render SwiftUI previews | `xcode_tools_RenderPreview` | (none) | Additive; we don’t have a preview renderer. | - -## High-confidence replacements (hide XcodeBuildMCP tool in favor of Xcode Tools MCP) - -These are the tools where the user expectation (“do it inside Xcode”) matches an Xcode Tools MCP tool, and using our external implementation is likely worse. - -### Builds - -Preferred (inside Xcode): `xcode_tools_BuildProject` (+ `xcode_tools_GetBuildLog` to read the result). - -Hide these XcodeBuildMCP tools when running under Xcode **and** Xcode Tools MCP is available: - -- `simulator/build_sim` -- `simulator/build_run_sim` -- `device/build_device` -- `macos/build_macos` -- `macos/build_run_macos` - -Rationale: -- These tools are conceptually “build in Xcode”; inside Xcode, the IDE’s own build orchestration should win. -- Even when they work, external builds can produce confusing logs/results compared to Xcode’s build log UI. - -Decision: -- Hide these tools in Xcode agent mode when Xcode Tools MCP is available. - -### Tests - -Preferred (inside Xcode): `xcode_tools_RunAllTests` / `xcode_tools_RunSomeTests` (+ `xcode_tools_GetTestList` for discovery). - -Hide these XcodeBuildMCP tools when running under Xcode **and** Xcode Tools MCP is available: - -- `simulator/test_sim` -- `device/test_device` -- `macos/test_macos` - -Optional (see “Medium-confidence” below): -- `swift-package/swift_package_test` - -Rationale: -- Same expectation mismatch as builds: inside Xcode, “run tests” should be IDE-driven for correct scheme/test plan selection and to keep results in the IDE’s world. - -Decision: -- Hide these tools in Xcode agent mode when Xcode Tools MCP is available. - -## Medium-confidence replacements (likely hide, but verify ergonomics first) - -These are tools where Xcode Tools MCP likely provides a better alternative, but the mapping isn’t 1:1 or may depend on project type. - -### Project/workspace discovery - -Candidate: -- `*/discover_projs` (appears in `simulator`, `device`, `macos`, `project-discovery`) - -Potential replacement (inside Xcode): -- `xcode_tools_XcodeListWindows` (gives you the active workspace + `tabIdentifier`) - -Recommendation: -- Hide `discover_projs` inside Xcode agent mode. -- Prefer tab-scoped context (`tabIdentifier`) over filesystem scanning to ensure builds/tests target the active Xcode workspace. - -Decision: -- Hide `discover_projs` in Xcode agent mode. -- Do not add a separate “window picker” tool; `XcodeListWindows` already provides the list and the agent can select the relevant `tabIdentifier`. - -### Swift Package builds/tests - -Potentially hide when inside Xcode and the package is opened in Xcode: - -- `swift-package/swift_package_build` -- `swift-package/swift_package_test` - -Preferred (inside Xcode): `xcode_tools_BuildProject` / `xcode_tools_RunAllTests`. - -Why it’s medium confidence: -- Our SwiftPM tools are valuable when working outside Xcode or for pure SwiftPM workflows. -- In Xcode agent mode, users may still want a “swift package” workflow if the package is not opened as an Xcode project/tab. - -Decision: -- Hide `swift_package_build` and `swift_package_test` in Xcode agent mode (regardless of project type detection), but keep them outside Xcode agent mode. - -### Build settings / scheme listing - -Candidates: -- `*/list_schemes` -- `*/show_build_settings` - -Xcode Tools MCP does not obviously provide a direct “list schemes” or “show build settings” equivalent; but the IDE *knows* these things and may surface them indirectly via build/test APIs (or via project navigator + metadata). - -Recommendation: -- Treat these as supporting tools for external `xcodebuild`-driven workflows. If we hide the external build/test workflows in Xcode agent mode, these are usually noise. - -Decision: -- Hide `*/list_schemes` and `*/show_build_settings` in Xcode agent mode if (after audit) no remaining exposed tool depends on them. -- If we still have any XcodeBuildMCP tool that requires scheme/config/build settings inside Xcode agent mode, keep these until that dependency is removed or replaced. - -## Low-confidence replacements (do not hide; keep XcodeBuildMCP) - -These remain useful inside Xcode because Xcode Tools MCP does not replace them, or because they operate on domains outside the IDE tool service. - -- Simulator/device control + UI automation: - - `simulator-management/*`, `simulator/boot_sim`, `simulator/open_sim`, `ui-automation/*` -- App install/launch/stop/log capture: - - `simulator/install_app_sim`, `simulator/launch_app_sim`, `simulator/stop_app_sim` - - `device/install_app_device`, `device/launch_app_device`, `device/stop_app_device` - - `logging/*` (device/sim log capture) -- Debugger workflows: - - `debugging/*` (LLDB/DAP/stack/breakpoints) - -Reasoning: -- Xcode Tools MCP is IDE-scoped and does not appear to cover simulator/device lifecycle automation at the level XcodeBuildMCP provides. -- Even if Xcode can run/debug, the agent often needs precise control (boot, tap, capture logs, etc.) that is outside the IDE tool service. - -Decision: -- Keep the simulator/device/debugging/logging automation tools. -- Hide `project-scaffolding/*` in Xcode agent mode by default (keep outside Xcode agent mode). - -## Concrete “hide” policy proposal (for implementation) - -When **all** conditions are true: - -1) `runningUnderXcode === true` (process-tree detection), and -2) Xcode Tools bridge is enabled/available (i.e. Xcode Tools MCP is in play), - -…then hide (do not register / do not list) the following XcodeBuildMCP tools: - -- Builds: `simulator/build_sim`, `simulator/build_run_sim`, `device/build_device`, `macos/build_macos`, `macos/build_run_macos` -- Tests: `simulator/test_sim`, `device/test_device`, `macos/test_macos` -- Discovery: `*/discover_projs` -- SwiftPM: `swift-package/swift_package_build`, `swift-package/swift_package_test` -- Scaffolding: `project-scaffolding/scaffold_ios_project`, `project-scaffolding/scaffold_macos_project` -- Clean: `*/clean` (unless we explicitly keep it as an escape hatch) - -Implementation note: -- “`*/clean`” maps to multiple workflows (`device/clean`, `macos/clean`, `simulator/clean`, `utilities/clean`). -- “`*/discover_projs`” maps to multiple workflows (`device/discover_projs`, `macos/discover_projs`, `simulator/discover_projs`, `project-discovery/discover_projs`). - -Notes: -- This should be “hide”, not “delete”: keep the tools as fallback when not running under Xcode, or when the bridge is unavailable/untrusted. -- Keep a config escape hatch (e.g. `preferXcodeToolsInXcodeAgent: true/false`) if you want to allow power users to force external builds/tests. - -## Interplay with workflow selection (`enabledWorkflows`) and workflow management - -There are two separate “filters” that decide what tools an agent sees: - -1) **Workflow selection** (coarse inclusion) - - The server registers tools from the workflows listed in config `enabledWorkflows` (plus mandatory workflows). - - If `enabledWorkflows` is empty, workflow selection defaults to the `simulator` workflow. - - The `manage-workflows` tool adjusts the *enabled workflow list* at runtime by adding/removing workflow names, then re-applying selection. - - Some workflows are effectively mandatory (e.g. `session-management`; `doctor` and `workflow-discovery` are auto-added based on config flags). - -2) **Xcode-agent-mode visibility filtering** (fine-grained hiding) - - This report proposes an additional post-selection rule that hides specific tools when running inside Xcode and Xcode Tools MCP is enabled/available. - - This is intentionally layered **after** workflow selection: a tool must be included by workflow selection *first* before it can be hidden by visibility filtering. - - Effect: enabling a workflow does not guarantee every tool in that workflow is visible in Xcode agent mode (by design). - -Important nuance: some tools are **re-exported** across workflows (same MCP tool name appears in multiple workflows). Disabling one workflow might not remove the tool if another enabled workflow still provides it; the visibility filter applies to the tool name regardless of where it came from. - -## Why this improves the Xcode agent UX - -- Matches the user’s mental model: “I’m in Xcode; builds/tests happen in Xcode”. -- Makes tool choice less noisy: the agent isn’t offered two competing ways to “build” or “run tests”. -- Reduces risk of confusing state divergence (derived data, schemes, logs, test results). - -## Follow-up validation tasks (before hard-hiding) - -These are quick checks to run once the Xcode Tools bridge is stable in the Xcode agent environment: - -- Confirm `BuildProject` supports the projects you care about (workspace vs project, SPM package, build configurations). -- Confirm test tools (`RunAllTests` / `RunSomeTests`) map cleanly to scheme/test plan selection and produce useful results. -- Confirm `GetBuildLog` provides enough structure for agents (errors/warnings + file locations). -- Confirm whether Xcode exposes “clean” semantics via `BuildProject` (or some other tool). If it does, `*/clean` may become a hide candidate too. -- Audit remaining exposed XcodeBuildMCP tools in Xcode agent mode to ensure none require schemes/build settings (otherwise keep `*/list_schemes` and `*/show_build_settings`). - -## Full tool catalog decisions (Xcode agent mode) - -The table below is the complete XcodeBuildMCP tool catalog (from `build/tools-manifest.json`) with the yes/no decision for whether to hide the tool when: - -- Running under Xcode is `true`, AND -- Xcode Tools MCP is enabled/available - -| Workflow | Tool | Hide In Xcode Agent Mode? | Notes | -| --- | --- | --- | --- | -| debugging | debug_attach_sim | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_breakpoint_add | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_breakpoint_remove | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_continue | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_detach | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_lldb_command | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_stack | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| debugging | debug_variables | No | No Xcode Tools equivalent; simulator debugging via LLDB/DAP. | -| device | build_device | Yes | Prefer `xcode_tools_BuildProject` (IDE build). | -| device | clean | Yes | External clean is surprising in IDE mode; keep as non-Xcode fallback only. Re-export of utilities. | -| device | discover_projs | Yes | Prefer `xcode_tools_XcodeListWindows` + `tabIdentifier` over filesystem scanning. Re-export of project-discovery. | -| device | get_app_bundle_id | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. Re-export of project-discovery. | -| device | get_device_app_path | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. | -| device | install_app_device | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. | -| device | launch_app_device | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. | -| device | list_devices | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. | -| device | list_schemes | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. Re-export of project-discovery. | -| device | show_build_settings | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. Re-export of project-discovery. | -| device | start_device_log_cap | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. Re-export of logging. | -| device | stop_app_device | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. | -| device | stop_device_log_cap | No | Runtime device workflow (install/launch/logs/etc) not covered by Xcode Tools MCP. Re-export of logging. | -| device | test_device | Yes | Prefer `xcode_tools_RunAllTests`/`xcode_tools_RunSomeTests` (IDE tests). | -| doctor | doctor | No | Diagnostics; still needed. | -| logging | start_device_log_cap | No | No Xcode Tools equivalent; log capture (sim/device). | -| logging | start_sim_log_cap | No | No Xcode Tools equivalent; log capture (sim/device). | -| logging | stop_device_log_cap | No | No Xcode Tools equivalent; log capture (sim/device). | -| logging | stop_sim_log_cap | No | No Xcode Tools equivalent; log capture (sim/device). | -| macos | build_macos | Yes | Prefer `xcode_tools_BuildProject` (IDE build). | -| macos | build_run_macos | Yes | Prefer `xcode_tools_BuildProject` (IDE build). | -| macos | clean | Yes | External clean is surprising in IDE mode; keep as non-Xcode fallback only. Re-export of utilities. | -| macos | discover_projs | Yes | Prefer `xcode_tools_XcodeListWindows` + `tabIdentifier` over filesystem scanning. Re-export of project-discovery. | -| macos | get_mac_app_path | No | Runtime macOS app lifecycle not covered by Xcode Tools MCP. | -| macos | get_mac_bundle_id | No | Runtime macOS app lifecycle not covered by Xcode Tools MCP. Re-export of project-discovery. | -| macos | launch_mac_app | No | Runtime macOS app lifecycle not covered by Xcode Tools MCP. | -| macos | list_schemes | No | Runtime macOS app lifecycle not covered by Xcode Tools MCP. Re-export of project-discovery. | -| macos | show_build_settings | No | Runtime macOS app lifecycle not covered by Xcode Tools MCP. Re-export of project-discovery. | -| macos | stop_mac_app | No | Runtime macOS app lifecycle not covered by Xcode Tools MCP. | -| macos | test_macos | Yes | Prefer `xcode_tools_RunAllTests`/`xcode_tools_RunSomeTests` (IDE tests). | -| project-discovery | discover_projs | Yes | Prefer `xcode_tools_XcodeListWindows` + `tabIdentifier` over filesystem scanning. | -| project-discovery | get_app_bundle_id | No | Useful for non-IDE flows and for remaining runtime tools. | -| project-discovery | get_mac_bundle_id | No | Useful for non-IDE flows and for remaining runtime tools. | -| project-discovery | list_schemes | No | Useful for non-IDE flows and for remaining runtime tools. | -| project-discovery | show_build_settings | No | Useful for non-IDE flows and for remaining runtime tools. | -| project-scaffolding | scaffold_ios_project | Yes | Not expected inside Xcode agent mode (project already open). | -| project-scaffolding | scaffold_macos_project | Yes | Not expected inside Xcode agent mode (project already open). | -| session-management | session_clear_defaults | No | Session defaults plumbing; still needed. | -| session-management | session_set_defaults | No | Session defaults plumbing; still needed. | -| session-management | session_show_defaults | No | Session defaults plumbing; still needed. | -| simulator | boot_sim | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | build_run_sim | Yes | Prefer `xcode_tools_BuildProject` (IDE build). | -| simulator | build_sim | Yes | Prefer `xcode_tools_BuildProject` (IDE build). | -| simulator | clean | Yes | External clean is surprising in IDE mode; keep as non-Xcode fallback only. Re-export of utilities. | -| simulator | discover_projs | Yes | Prefer `xcode_tools_XcodeListWindows` + `tabIdentifier` over filesystem scanning. Re-export of project-discovery. | -| simulator | get_app_bundle_id | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. Re-export of project-discovery. | -| simulator | get_sim_app_path | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | install_app_sim | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | launch_app_logs_sim | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | launch_app_sim | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | list_schemes | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. Re-export of project-discovery. | -| simulator | list_sims | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | open_sim | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | record_sim_video | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | screenshot | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. Re-export of ui-automation. | -| simulator | show_build_settings | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. Re-export of project-discovery. | -| simulator | snapshot_ui | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. Re-export of ui-automation. | -| simulator | stop_app_sim | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. | -| simulator | stop_sim_log_cap | No | Runtime simulator workflow (install/launch/UI/etc) not covered by Xcode Tools MCP. Re-export of logging. | -| simulator | test_sim | Yes | Prefer `xcode_tools_RunAllTests`/`xcode_tools_RunSomeTests` (IDE tests). | -| simulator-management | boot_sim | No | Simulator fleet management not covered by Xcode Tools MCP. Re-export of simulator. | -| simulator-management | erase_sims | No | Simulator fleet management not covered by Xcode Tools MCP. | -| simulator-management | list_sims | No | Simulator fleet management not covered by Xcode Tools MCP. Re-export of simulator. | -| simulator-management | open_sim | No | Simulator fleet management not covered by Xcode Tools MCP. Re-export of simulator. | -| simulator-management | reset_sim_location | No | Simulator fleet management not covered by Xcode Tools MCP. | -| simulator-management | set_sim_appearance | No | Simulator fleet management not covered by Xcode Tools MCP. | -| simulator-management | set_sim_location | No | Simulator fleet management not covered by Xcode Tools MCP. | -| simulator-management | sim_statusbar | No | Simulator fleet management not covered by Xcode Tools MCP. | -| swift-package | swift_package_build | Yes | Build/test should run via Xcode IDE in agent mode. | -| swift-package | swift_package_clean | No | Keep (no clear IDE-scoped replacement). | -| swift-package | swift_package_list | No | Keep (no clear IDE-scoped replacement). | -| swift-package | swift_package_run | No | Keep (no clear IDE-scoped replacement). | -| swift-package | swift_package_stop | No | Keep (no clear IDE-scoped replacement). | -| swift-package | swift_package_test | Yes | Build/test should run via Xcode IDE in agent mode. | -| ui-automation | button | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | gesture | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | key_press | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | key_sequence | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | long_press | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | screenshot | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | snapshot_ui | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | swipe | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | tap | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | touch | No | No Xcode Tools equivalent; simulator UI automation. | -| ui-automation | type_text | No | No Xcode Tools equivalent; simulator UI automation. | -| utilities | clean | Yes | External clean is surprising in IDE mode; keep as non-Xcode fallback only. | -| workflow-discovery | manage_workflows | No | Workflow toggling; still needed. | -| xcode-ide | xcode_tools_bridge_disconnect | No | Bridge debug-only tools (also gated by `debug: true`). | -| xcode-ide | xcode_tools_bridge_status | No | Bridge debug-only tools (also gated by `debug: true`). | -| xcode-ide | xcode_tools_bridge_sync | No | Bridge debug-only tools (also gated by `debug: true`). | diff --git a/docs/dev/ZOD_MIGRATION_GUIDE.md b/docs/dev/ZOD_MIGRATION_GUIDE.md deleted file mode 100644 index b3a638cc..00000000 --- a/docs/dev/ZOD_MIGRATION_GUIDE.md +++ /dev/null @@ -1,879 +0,0 @@ -# Migration guide - -import { Callout } from "fumadocs-ui/components/callout"; -import { Tabs, Tab } from "fumadocs-ui/components/tabs"; - -This migration guide aims to list the breaking changes in Zod 4 in order of highest to lowest impact. To learn more about the performance enhancements and new features of Zod 4, read the [introductory post](/v4). - -{/* To give the ecosystem time to migrate, Zod 4 will initially be published alongside Zod v3.25. To use Zod 4, upgrade to `zod@3.25.0` or later: */} - -``` -npm install zod@^4.0.0 -``` - -{/* Zod 4 is available at the `"/v4"` subpath: - - ```ts - import * as z from "zod"; - ``` */} - -Many of Zod's behaviors and APIs have been made more intuitive and cohesive. The breaking changes described in this document often represent major quality-of-life improvements for Zod users. I strongly recommend reading this guide thoroughly. - - - **Note** — Zod 3 exported a number of undocumented quasi-internal utility types and functions that are not considered part of the public API. Changes to those are not documented here. - - - - **Unofficial codemod** — A community-maintained codemod [`zod-v3-to-v4`](https://github.com/nicoespeon/zod-v3-to-v4) is available. - - -## Error customization - -Zod 4 standardizes the APIs for error customization under a single, unified `error` param. Previously Zod's error customization APIs were fragmented and inconsistent. This is cleaned up in Zod 4. - -### deprecates `message` - -Replaces `message` with `error`. The `message` parameter is still supported but deprecated. - - - - ```ts - z.string().min(5, { error: "Too short." }); - ``` - - - - ```ts - z.string().min(5, { message: "Too short." }); - ``` - - - -### drops `invalid_type_error` and `required_error` - -The `invalid_type_error` / `required_error` params have been dropped. These were hastily added years ago as a way to customize errors that was less verbose than `errorMap`. They came with all sorts of footguns (they can't be used in conjunction with `errorMap`) and do not align with Zod's actual issue codes (there is no `required` issue code). - -These can now be cleanly represented with the new `error` parameter. - - - - ```ts - z.string({ - error: (issue) => issue.input === undefined - ? "This field is required" - : "Not a string" - }); - ``` - - - - ```ts - z.string({ - required_error: "This field is required", - invalid_type_error: "Not a string", - }); - ``` - - - -### drops `errorMap` - -This is renamed to `error`. - -Error maps can also now return a plain `string` (instead of `{message: string}`). They can also return `undefined`, which tells Zod to yield control to the next error map in the chain. - - - - ```ts - z.string().min(5, { - error: (issue) => { - if (issue.code === "too_small") { - return `Value must be >${issue.minimum}` - } - }, - }); - ``` - - - - ```ts - z.string({ - errorMap: (issue, ctx) => { - if (issue.code === "too_small") { - return { message: `Value must be >${issue.minimum}` }; - } - return { message: ctx.defaultError }; - }, - }); - ``` - - - -{/* ## `.safeParse()` - - For performance reasons, the errors returned by `.safeParse()` and `.safeParseAsync()` no longer extend `Error`. - - ```ts - const result = z.string().safeParse(12); - result.error! instanceof Error; // => false - ``` - - It is very slow to instantiate `Error` instances in JavaScript, as the initialization process snapshots the call stack. In the case of Zod's "safe" parse methods, it's expected that you will handle errors at the point of parsing, so instantiating a true `Error` object adds little value anyway. - - > Pro tip: prefer `.safeParse()` over `try/catch` in performance-sensitive code. - - By contrast the errors thrown by `.parse()` and `.parseAsync()` still extend `Error`. Aside from the prototype difference, the error classes are identical. - - ```ts - try { - z.string().parse(12); - } catch (err) { - console.log(err instanceof Error); // => true - } - ``` - */} - -## `ZodError` - -{/* - ### changes to `.message` - - Previously the `.message` property on `ZodError` was a JSON.stringified copy of the `.issues` array. This was redundant, confusing, and a bit of an abuse of the `.message` property. Also due to the [`Error` prototype changes](#safeparse) (and inconsistencies in how Node.js logs `Error` subclasses vs other objects) the logging of a multi-line `.message` property got a lot uglier: - - ```sh - $ tsx index.ts - ZodError { - message: '[\n' + - ' {\n' + - ' "expected": "string",\n' + - ' "code": "invalid_type",\n' + - ' "path": [],\n' + - ' "message": "Invalid input: expected string, received number"\n' + - ' }\n' + - ']' - } - ``` - - - For these reasons, the `.message` property is left empty and the `.issues` array is marked as enumerable. This keeps error logging consistent and pretty: - - ```sh - $ tsx index.ts - z.string().parse(234); - - ZodError { - issues: [ - { - expected: 'string', - code: 'invalid_type', - path: [], - message: 'Invalid input: expected string, received number' - } - ] - } - ``` - - Vitest uses special handling for `Error` subclasses that ignores enumerable properties. */} - -### updates issue formats - -The issue formats have been dramatically streamlined. - -```ts -import * as z from "zod"; // v4 - -type IssueFormats = - | z.core.$ZodIssueInvalidType - | z.core.$ZodIssueTooBig - | z.core.$ZodIssueTooSmall - | z.core.$ZodIssueInvalidStringFormat - | z.core.$ZodIssueNotMultipleOf - | z.core.$ZodIssueUnrecognizedKeys - | z.core.$ZodIssueInvalidValue - | z.core.$ZodIssueInvalidUnion - | z.core.$ZodIssueInvalidKey // new: used for z.record/z.map - | z.core.$ZodIssueInvalidElement // new: used for z.map/z.set - | z.core.$ZodIssueCustom; -``` - -Below is the list of Zod 3 issues types and their Zod 4 equivalent: - -```ts -import * as z from "zod"; // v3 - -export type IssueFormats = - | z.ZodInvalidTypeIssue // ♻️ renamed to z.core.$ZodIssueInvalidType - | z.ZodTooBigIssue // ♻️ renamed to z.core.$ZodIssueTooBig - | z.ZodTooSmallIssue // ♻️ renamed to z.core.$ZodIssueTooSmall - | z.ZodInvalidStringIssue // ♻️ z.core.$ZodIssueInvalidStringFormat - | z.ZodNotMultipleOfIssue // ♻️ renamed to z.core.$ZodIssueNotMultipleOf - | z.ZodUnrecognizedKeysIssue // ♻️ renamed to z.core.$ZodIssueUnrecognizedKeys - | z.ZodInvalidUnionIssue // ♻️ renamed to z.core.$ZodIssueInvalidUnion - | z.ZodCustomIssue // ♻️ renamed to z.core.$ZodIssueCustom - | z.ZodInvalidEnumValueIssue // ❌ merged in z.core.$ZodIssueInvalidValue - | z.ZodInvalidLiteralIssue // ❌ merged into z.core.$ZodIssueInvalidValue - | z.ZodInvalidUnionDiscriminatorIssue // ❌ throws an Error at schema creation time - | z.ZodInvalidArgumentsIssue // ❌ z.function throws ZodError directly - | z.ZodInvalidReturnTypeIssue // ❌ z.function throws ZodError directly - | z.ZodInvalidDateIssue // ❌ merged into invalid_type - | z.ZodInvalidIntersectionTypesIssue // ❌ removed (throws regular Error) - | z.ZodNotFiniteIssue // ❌ infinite values no longer accepted (invalid_type) -``` - -While certain Zod 4 issue types have been merged, dropped, and modified, each issue remains structurally similar to Zod 3 counterpart (identical, in most cases). All issues still conform to the same base interface as Zod 3, so most common error handling logic will work without modification. - -```ts -export interface $ZodIssueBase { - readonly code?: string; - readonly input?: unknown; - readonly path: PropertyKey[]; - readonly message: string; -} -``` - -### changes error map precedence - -The error map precedence has been changed to be more consistent. Specifically, an error map passed into `.parse()` *no longer* takes precedence over a schema-level error map. - -```ts -const mySchema = z.string({ error: () => "Schema-level error" }); - -// in Zod 3 -mySchema.parse(12, { error: () => "Contextual error" }); // => "Contextual error" - -// in Zod 4 -mySchema.parse(12, { error: () => "Contextual error" }); // => "Schema-level error" -``` - -### deprecates `.format()` - -The `.format()` method on `ZodError` has been deprecated. Instead use the top-level `z.treeifyError()` function. Read the [Formatting errors docs](/error-formatting) for more information. - -### deprecates `.flatten()` - -The `.flatten()` method on `ZodError` has also been deprecated. Instead use the top-level `z.treeifyError()` function. Read the [Formatting errors docs](/error-formatting) for more information. - -### drops `.formErrors` - -This API was identical to `.flatten()`. It exists for historical reasons and isn't documented. - -### deprecates `.addIssue()` and `.addIssues()` - -Directly push to `err.issues` array instead, if necessary. - -```ts -myError.issues.push({ - // new issue -}); -``` - -{/* ## `.and()` dropped - - The `.and()` method on `ZodType` has been dropped in favor of `z.intersection(A, B)`. Not only is this method rarely used, there are few good reasons to use intersections at all. The `.and()` API prevented bundlers from treeshaking `ZodIntersection`, a fairly large and complex class. - - ```ts - z.object({ a: z.string() }).and(z.object({ b: z.number() })); // ❌ - - // use z.intersection - z.intersection(z.object({ a: z.string() }), z.object({ b: z.number() })); // ✅ - // or .extend() when possible - z.object({ a: z.string() }).extend(z.object({ b: z.number() })); // ✅ - ``` */} - -## `z.number()` - -### no infinite values - -`POSITIVE_INFINITY` and `NEGATIVE_INFINITY` are no longer considered valid values for `z.number()`. - -### `.safe()` no longer accepts floats - -In Zod 3, `z.number().safe()` is deprecated. It now behaves identically to `.int()` (see below). Importantly, that means it no longer accepts floats. - -### `.int()` accepts safe integers only - -The `z.number().int()` API no longer accepts unsafe integers (outside the range of `Number.MIN_SAFE_INTEGER` and `Number.MAX_SAFE_INTEGER`). Using integers out of this range causes spontaneous rounding errors. (Also: You should switch to `z.int()`.) - -## `z.string()` updates - -### deprecates `.email()` etc - -String formats are now represented as *subclasses* of `ZodString`, instead of simple internal refinements. As such, these APIs have been moved to the top-level `z` namespace. Top-level APIs are also less verbose and more tree-shakable. - -```ts -z.email(); -z.uuid(); -z.url(); -z.emoji(); // validates a single emoji character -z.base64(); -z.base64url(); -z.nanoid(); -z.cuid(); -z.cuid2(); -z.ulid(); -z.ipv4(); -z.ipv6(); -z.cidrv4(); // ip range -z.cidrv6(); // ip range -z.iso.date(); -z.iso.time(); -z.iso.datetime(); -z.iso.duration(); -``` - -The method forms (`z.string().email()`) still exist and work as before, but are now deprecated. - -```ts -z.string().email(); // ❌ deprecated -z.email(); // ✅ -``` - -### stricter `.uuid()` - -The `z.uuid()` now validates UUIDs more strictly against the RFC 9562/4122 specification; specifically, the variant bits must be `10` per the spec. For a more permissive "UUID-like" validator, use `z.guid()`. - -```ts -z.uuid(); // RFC 9562/4122 compliant UUID -z.guid(); // any 8-4-4-4-12 hex pattern -``` - -### no padding in `.base64url()` - -Padding is no longer allowed in `z.base64url()` (formerly `z.string().base64url()`). Generally it's desirable for base64url strings to be unpadded and URL-safe. - -### drops `z.string().ip()` - -This has been replaced with separate `.ipv4()` and `.ipv6()` methods. Use `z.union()` to combine them if you need to accept both. - -```ts -z.string().ip() // ❌ -z.ipv4() // ✅ -z.ipv6() // ✅ -``` - -### updates `z.string().ipv6()` - -Validation now happens using the `new URL()` constructor, which is far more robust than the old regular expression approach. Some invalid values that passed validation previously may now fail. - -### drops `z.string().cidr()` - -Similarly, this has been replaced with separate `.cidrv4()` and `.cidrv6()` methods. Use `z.union()` to combine them if you need to accept both. - -```ts -z.string().cidr() // ❌ -z.cidrv4() // ✅ -z.cidrv6() // ✅ -``` - -## `z.coerce` updates - -The input type of all `z.coerce` schemas is now `unknown`. - -```ts -const schema = z.coerce.string(); -type schemaInput = z.input; - -// Zod 3: string; -// Zod 4: unknown; -``` - -## `.default()` updates - -The application of `.default()` has changed in a subtle way. If the input is `undefined`, `ZodDefault` short-circuits the parsing process and returns the default value. The default value must be assignable to the *output type*. - -```ts -const schema = z.string() - .transform(val => val.length) - .default(0); // should be a number -schema.parse(undefined); // => 0 -``` - -In Zod 3, `.default()` expected a value that matched the *input type*. `ZodDefault` would parse the default value, instead of short-circuiting. As such, the default value must be assignable to the *input type* of the schema. - -```ts -// Zod 3 -const schema = z.string() - .transform(val => val.length) - .default("tuna"); -schema.parse(undefined); // => 4 -``` - -To replicate the old behavior, Zod implements a new `.prefault()` API. This is short for "pre-parse default". - -```ts -// Zod 3 -const schema = z.string() - .transform(val => val.length) - .prefault("tuna"); -schema.parse(undefined); // => 4 -``` - -## `z.object()` - -### defaults applied within optional fields - -Defaults inside your properties are applied, even within optional fields. This aligns better with expectations and resolves a long-standing usability issue with Zod 3. This is a subtle change that may cause breakage in code paths that rely on key existence, etc. - -```ts -const schema = z.object({ - a: z.string().default("tuna").optional(), -}); - -schema.parse({}); -// Zod 4: { a: "tuna" } -// Zod 3: {} -``` - -### deprecates `.strict()` and `.passthrough()` - -These methods are generally no longer necessary. Instead use the top-level `z.strictObject()` and `z.looseObject()` functions. - -```ts -// Zod 3 -z.object({ name: z.string() }).strict(); -z.object({ name: z.string() }).passthrough(); - -// Zod 4 -z.strictObject({ name: z.string() }); -z.looseObject({ name: z.string() }); -``` - -> These methods are still available for backwards compatibility, and they will not be removed. They are considered legacy. - -### deprecates `.strip()` - -This was never particularly useful, as it was the default behavior of `z.object()`. To convert a strict object to a "regular" one, use `z.object(A.shape)`. - -### drops `.nonstrict()` - -This long-deprecated alias for `.strip()` has been removed. - -### drops `.deepPartial()` - -This has been long deprecated in Zod 3 and it now removed in Zod 4. There is no direct alternative to this API. There were lots of footguns in its implementation, and its use is generally an anti-pattern. - -### changes `z.unknown()` optionality - -The `z.unknown()` and `z.any()` types are no longer marked as "key optional" in the inferred types. - -```ts -const mySchema = z.object({ - a: z.any(), - b: z.unknown() -}); -// Zod 3: { a?: any; b?: unknown }; -// Zod 4: { a: any; b: unknown }; -``` - -### deprecates `.merge()` - -The `.merge()` method on `ZodObject` has been deprecated in favor of `.extend()`. The `.extend()` method provides the same functionality, avoids ambiguity around strictness inheritance, and has better TypeScript performance. - -```ts -// .merge (deprecated) -const ExtendedSchema = BaseSchema.merge(AdditionalSchema); - -// .extend (recommended) -const ExtendedSchema = BaseSchema.extend(AdditionalSchema.shape); - -// or use destructuring (best tsc performance) -const ExtendedSchema = z.object({ - ...BaseSchema.shape, - ...AdditionalSchema.shape, -}); -``` - -> **Note**: For even better TypeScript performance, consider using object destructuring instead of `.extend()`. See the [API documentation](/api?id=extend) for more details. - -## `z.nativeEnum()` deprecated - -The `z.nativeEnum()` function is now deprecated in favor of just `z.enum()`. The `z.enum()` API has been overloaded to support an enum-like input. - -```ts -enum Color { - Red = "red", - Green = "green", - Blue = "blue", -} - -const ColorSchema = z.enum(Color); // ✅ -``` - -As part of this refactor of `ZodEnum`, a number of long-deprecated and redundant features have been removed. These were all identical and only existed for historical reasons. - -```ts -ColorSchema.enum.Red; // ✅ => "Red" (canonical API) -ColorSchema.Enum.Red; // ❌ removed -ColorSchema.Values.Red; // ❌ removed -``` - -## `z.array()` - -### changes `.nonempty()` type - -This now behaves identically to `z.array().min(1)`. The inferred type does not change. - -```ts -const NonEmpty = z.array(z.string()).nonempty(); - -type NonEmpty = z.infer; -// Zod 3: [string, ...string[]] -// Zod 4: string[] -``` - -The old behavior is now better represented with `z.tuple()` and a "rest" argument. This aligns more closely to TypeScript's type system. - -```ts -z.tuple([z.string()], z.string()); -// => [string, ...string[]] -``` - -## `z.promise()` deprecated - -There's rarely a reason to use `z.promise()`. If you have an input that may be a `Promise`, just `await` it before parsing it with Zod. - -> If you are using `z.promise` to define an async function with `z.function()`, that's no longer necessary either; see the [`ZodFunction`](#function) section below. - -## `z.function()` - -The result of `z.function()` is no longer a Zod schema. Instead, it acts as a standalone "function factory" for defining Zod-validated functions. The API has also changed; you define an `input` and `output` schema upfront, instead of using `args()` and `.returns()` methods. - - - - ```ts - const myFunction = z.function({ - input: [z.object({ - name: z.string(), - age: z.number().int(), - })], - output: z.string(), - }); - - myFunction.implement((input) => { - return `Hello ${input.name}, you are ${input.age} years old.`; - }); - ``` - - - - ```ts - const myFunction = z.function() - .args(z.object({ - name: z.string(), - age: z.number().int(), - })) - .returns(z.string()); - - myFunction.implement((input) => { - return `Hello ${input.name}, you are ${input.age} years old.`; - }); - ``` - - - -If you have a desperate need for a Zod schema with a function type, consider [this workaround](https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912). - -### adds `.implementAsync()` - -To define an async function, use `implementAsync()` instead of `implement()`. - -```ts -myFunction.implementAsync(async (input) => { - return `Hello ${input.name}, you are ${input.age} years old.`; -}); -``` - -## `.refine()` - -### ignores type predicates - -In Zod 3, passing a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) as a refinement functions could still narrow the type of a schema. This wasn't documented but was discussed in some issues. This is no longer the case. - -```ts -const mySchema = z.unknown().refine((val): val is string => { - return typeof val === "string" -}); - -type MySchema = z.infer; -// Zod 3: `string` -// Zod 4: still `unknown` -``` - -### drops `ctx.path` - -Zod's new parsing architecture does not eagerly evaluate the `path` array. This was a necessary change that unlocks Zod 4's dramatic performance improvements. - -```ts -z.string().superRefine((val, ctx) => { - ctx.path; // ❌ no longer available -}); -``` - -### drops function as second argument - -The following horrifying overload has been removed. - -```ts -const longString = z.string().refine( - (val) => val.length > 10, - (val) => ({ message: `${val} is not more than 10 characters` }) -); -``` - -{/* ## `.superRefine()` deprecated - - The `.superRefine()` method has been deprecated in favor of `.check()`. The `.check()` method provides the same functionality with a cleaner API. The `.check()` method is also available on Zod and Zod Mini schemas. - - ```ts - const UniqueStringArray = z.array(z.string()).check((ctx) => { - if (ctx.value.length > 3) { - ctx.issues.push({ - code: "too_big", - maximum: 3, - origin: "array", - inclusive: true, - message: "Too many items 😡", - input: ctx.value - }); - } - - if (ctx.value.length !== new Set(ctx.value).size) { - ctx.issues.push({ - code: "custom", - message: `No duplicates allowed.`, - input: ctx.value - }); - } - }); - ``` */} - -## `z.ostring()`, etc dropped - -The undocumented convenience methods `z.ostring()`, `z.onumber()`, etc. have been removed. These were shorthand methods for defining optional string schemas. - -## `z.literal()` - -### drops `symbol` support - -Symbols aren't considered literal values, nor can they be simply compared with `===`. This was an oversight in Zod 3. - -## static `.create()` factories dropped - -Previously all Zod classes defined a static `.create()` method. These are now implemented as standalone factory functions. - -```ts -z.ZodString.create(); // ❌ -``` - -## `z.record()` - -### drops single argument usage - -Before, `z.record()` could be used with a single argument. This is no longer supported. - -```ts -// Zod 3 -z.record(z.string()); // ✅ - -// Zod 4 -z.record(z.string()); // ❌ -z.record(z.string(), z.string()); // ✅ -``` - -### improves enum support - -Records have gotten a lot smarter. In Zod 3, passing an enum into `z.record()` as a key schema would result in a partial type - -```ts -const myRecord = z.record(z.enum(["a", "b", "c"]), z.number()); -// { a?: number; b?: number; c?: number; } -``` - -In Zod 4, this is no longer the case. The inferred type is what you'd expect, and Zod ensures exhaustiveness; that is, it makes sure all enum keys exist in the input during parsing. - -```ts -const myRecord = z.record(z.enum(["a", "b", "c"]), z.number()); -// { a: number; b: number; c: number; } -``` - -To replicate the old behavior with optional keys, use `z.partialRecord()`: - -```ts -const myRecord = z.partialRecord(z.enum(["a", "b", "c"]), z.number()); -// { a?: number; b?: number; c?: number; } -``` - -## `z.intersection()` - -### throws `Error` on merge conflict - -Zod intersection parses the input against two schemas, then attempts to merge the results. In Zod 3, when the results were unmergable, Zod threw a `ZodError` with a special `"invalid_intersection_types"` issue. - -In Zod 4, this will throw a regular `Error` instead. The existence of unmergable results indicates a structural problem with the schema: an intersection of two incompatible types. Thus, a regular error is more appropriate than a validation error. - -## Internal changes - -> The typical user of Zod can likely ignore everything below this line. These changes do not impact the user-facing `z` APIs. - -There are too many internal changes to list here, but some may be relevant to regular users who are (intentionally or not) relying on certain implementation details. These changes will be of particular interest to library authors building tools on top of Zod. - -### updates generics - -The generic structure of several classes has changed. Perhaps most significant is the change to the `ZodType` base class: - -```ts -// Zod 3 -class ZodType { - // ... -} - -// Zod 4 -class ZodType { - // ... -} -``` - -The second generic `Def` has been entirely removed. Instead the base class now only tracks `Output` and `Input`. While previously the `Input` value defaulted to `Output`, it now defaults to `unknown`. This allows generic functions involving `z.ZodType` to behave more intuitively in many cases. - -```ts -function inferSchema(schema: T): T { - return schema; -}; - -inferSchema(z.string()); // z.ZodString -``` - -The need for `z.ZodTypeAny` has been eliminated; just use `z.ZodType` instead. - -### adds `z.core` - -Many utility functions and types have been moved to the new `zod/v4/core` sub-package, to facilitate code sharing between Zod and Zod Mini. - -```ts -import * as z from "zod/v4/core"; - -function handleError(iss: z.$ZodError) { - // do stuff -} -``` - -For convenience, the contents of `zod/v4/core` are also re-exported from `zod` and `zod/mini` under the `z.core` namespace. - -```ts -import * as z from "zod"; - -function handleError(iss: z.core.$ZodError) { - // do stuff -} -``` - -Refer to the [Zod Core](/packages/core) docs for more information on the contents of the core sub-library. - -### moves `._def` - -The `._def` property is now moved to `._zod.def`. The structure of all internal defs is subject to change; this is relevant to library authors but won't be comprehensively documented here. - -### drops `ZodEffects` - -This doesn't affect the user-facing APIs, but it's an internal change worth highlighting. It's part of a larger restructure of how Zod handles *refinements*. - -Previously both refinements and transformations lived inside a wrapper class called `ZodEffects`. That means adding either one to a schema would wrap the original schema in a `ZodEffects` instance. In Zod 4, refinements now live inside the schemas themselves. More accurately, each schema contains an array of "checks"; the concept of a "check" is new in Zod 4 and generalizes the concept of a refinement to include potentially side-effectful transforms like `z.toLowerCase()`. - -This is particularly apparent in the Zod Mini API, which heavily relies on the `.check()` method to compose various validations together. - -```ts -import * as z from "zod/mini"; - -z.string().check( - z.minLength(10), - z.maxLength(100), - z.toLowerCase(), - z.trim(), -); -``` - -### adds `ZodTransform` - -Meanwhile, transforms have been moved into a dedicated `ZodTransform` class. This schema class represents an input transform; in fact, you can actually define standalone transformations now: - -```ts -import * as z from "zod"; - -const schema = z.transform(input => String(input)); - -schema.parse(12); // => "12" -``` - -This is primarily used in conjunction with `ZodPipe`. The `.transform()` method now returns an instance of `ZodPipe`. - -```ts -z.string().transform(val => val); // ZodPipe -``` - -### drops `ZodPreprocess` - -As with `.transform()`, the `z.preprocess()` function now returns a `ZodPipe` instance instead of a dedicated `ZodPreprocess` instance. - -```ts -z.preprocess(val => val, z.string()); // ZodPipe -``` - -### drops `ZodBranded` - -Branding is now handled with a direct modification to the inferred type, instead of a dedicated `ZodBranded` class. The user-facing APIs remain the same. - -{/* - Dropping support for ES5 - - Zod relies on `Set` internally */} - -{/* - `z.keyof` now returns `ZodEnum` instead of `ZodLiteral` */} - -{/* ## Changed: `.refine()` - - The `.refine()` method used to accept a function as the second argument. - - ```ts - // no longer supported - const longString = z.string().refine( - (val) => val.length > 10, - (val) => ({ message: `${val} is not more than 10 characters` }) - ); - ``` - - This can be better represented with the new `error` parameter, so this overload has been removed. - - ```ts - const longString = z.string().refine((val) => val.length > 10, { - error: (issue) => `${issue.input} is not more than 10 characters`, - }); - `` - */} - -{/* - - No support for `null` or `undefined` in `z.literal` - - `z.literal(null)` - - `z.literal(undefined)` - - this was never documented */} - -{/* - Array min/max/length checks now run after parsing. This means they won't run if the parse has already aborted. */} - -{/* - Drops single-argument `z.record()` */} - -{/* - Smarter `z.record`: no longer Partial by default */} - -{/* - Intersection merge errors are now thrown as Error not ZodError - - These usually do not reflect a parse error but a structural problem with the schema */} - -{/* - Consolidates `unknownKeys` and `catchall` in ZodObject */} - -{/* - Dropping - - `ZodBranded`: purely a static-domain annotation - - `ZodFunction` */} - -{/* - The `description` is now stored in `z.defaultRegistry`, not the def - - No support for `description` in factory params - - Descriptions do not cascade in `.optional()`, etc */} - -{/* - Enums: - - ZodEnum and ZodNativeEnum are merged - - `.Values` and `.Enum` are removed. Use `.enum` instead. - - `.options` is removed */} diff --git a/docs/dev/session-aware-migration-todo.md b/docs/dev/session-aware-migration-todo.md deleted file mode 100644 index 4230f338..00000000 --- a/docs/dev/session-aware-migration-todo.md +++ /dev/null @@ -1,64 +0,0 @@ -# Session-Aware Migration TODO - -_Audit date: October 6, 2025_ - -Reference: [session_management_plan.md](session_management_plan.md) - -## Utilities -- [x] `src/mcp/tools/utilities/clean.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`. - -## Project Discovery -- [x] `src/mcp/tools/project-discovery/list_schemes.ts` — session defaults: `projectPath`, `workspacePath`. -- [x] `src/mcp/tools/project-discovery/show_build_settings.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`. - -## Device Workflows -- [x] `src/mcp/tools/device/build_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`. -- [x] `src/mcp/tools/device/test_device.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `deviceId`, `configuration`. -- [x] `src/mcp/tools/device/get_device_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`. -- [x] `src/mcp/tools/device/install_app_device.ts` — session defaults: `deviceId`. -- [x] `src/mcp/tools/device/launch_app_device.ts` — session defaults: `deviceId`. -- [x] `src/mcp/tools/device/stop_app_device.ts` — session defaults: `deviceId`. - -## Device Logging -- [x] `src/mcp/tools/logging/start_device_log_cap.ts` — session defaults: `deviceId`. - -## macOS Workflows -- [x] `src/mcp/tools/macos/build_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`. -- [x] `src/mcp/tools/macos/build_run_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`. -- [x] `src/mcp/tools/macos/test_macos.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`. -- [x] `src/mcp/tools/macos/get_mac_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `configuration`, `arch`. - -## Simulator Build/Test/Path -- [x] `src/mcp/tools/simulator/test_sim.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`. -- [x] `src/mcp/tools/simulator/get_sim_app_path.ts` — session defaults: `projectPath`, `workspacePath`, `scheme`, `simulatorId`, `simulatorName`, `configuration`, `useLatestOS`, `arch`. - -## Simulator Runtime Actions -- [x] `src/mcp/tools/simulator/boot_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator/install_app_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator/launch_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator/launch_app_logs_sim.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator/stop_app_sim.ts` — session defaults: `simulatorId`, `simulatorName` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator/record_sim_video.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). - -## Simulator Management -- [x] `src/mcp/tools/simulator-management/erase_sims.ts` — session defaults: `simulatorId` (covers `simulatorUdid`). -- [x] `src/mcp/tools/simulator-management/set_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator-management/reset_sim_location.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator-management/set_sim_appearance.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/simulator-management/sim_statusbar.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). - -## Simulator Logging -- [x] `src/mcp/tools/logging/start_sim_log_cap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). - -## AXe UI Testing Tools -- [x] `src/mcp/tools/ui-testing/button.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/snapshot_ui.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/gesture.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/key_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/key_sequence.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/long_press.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/screenshot.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/swipe.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/tap.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/touch.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/type_text.ts` — session defaults: `simulatorId` (hydrate `simulatorUuid`). diff --git a/docs/dev/session_management_plan.md b/docs/dev/session_management_plan.md deleted file mode 100644 index 9c539e7c..00000000 --- a/docs/dev/session_management_plan.md +++ /dev/null @@ -1,482 +0,0 @@ -# Stateful Session Defaults for MCP Tools — Design, Middleware, and Plan - -Below is a concise architecture and implementation plan to introduce a session-aware defaults layer that removes repeated tool parameters from public schemas, while keeping all tool logic and tests unchanged. - -## Architecture Overview - -- **Core idea**: keep logic functions and tests untouched; move argument consolidation into a session-aware interop layer and expose minimal public schemas. -- **Data flow**: - - Client calls a tool with zero or few args → session middleware merges session defaults → validates with the internal schema → calls the existing logic function. -- **Components**: - - `SessionStore` (singleton, in-memory): set/get/clear/show defaults. - - Session-aware tool factory: merges defaults, performs preflight requirement checks (allOf/oneOf), then validates with the tool's internal zod schema. - - Public vs internal schema: plugins register a minimal "public" input schema; handlers validate with the unchanged "internal" schema. - -## Core Types - -```typescript -// src/utils/session-store.ts -export type SessionDefaults = { - projectPath?: string; - workspacePath?: string; - scheme?: string; - configuration?: string; - simulatorName?: string; - simulatorId?: string; - deviceId?: string; - useLatestOS?: boolean; - arch?: 'arm64' | 'x86_64'; -}; -``` - -## Session Store (singleton) - -```typescript -// src/utils/session-store.ts -import { log } from './logger.ts'; - -class SessionStore { - private defaults: SessionDefaults = {}; - - setDefaults(partial: Partial): void { - this.defaults = { ...this.defaults, ...partial }; - log('info', '[Session] Defaults set', { keys: Object.keys(partial) }); - } - - clear(keys?: (keyof SessionDefaults)[]): void { - if (!keys || keys.length === 0) { - this.defaults = {}; - log('info', '[Session] All defaults cleared'); - return; - } - for (const k of keys) delete this.defaults[k]; - log('info', '[Session] Defaults cleared', { keys }); - } - - get(key: K): SessionDefaults[K] { - return this.defaults[key]; - } - - getAll(): SessionDefaults { - return { ...this.defaults }; - } -} - -export const sessionStore = new SessionStore(); -``` - -## Session-Aware Tool Factory - -```typescript -// src/utils/typed-tool-factory.ts (add new helper, keep createTypedTool as-is) -import { z } from 'zod'; -import { sessionStore, type SessionDefaults } from './session-store.ts'; -import type { CommandExecutor } from './execution/index.ts'; -import { createErrorResponse } from './responses/index.ts'; -import type { ToolResponse } from '../types/common.ts'; - -export type SessionRequirement = - | { allOf: (keyof SessionDefaults)[]; message?: string } - | { oneOf: (keyof SessionDefaults)[]; message?: string }; - -function missingFromArgsAndSession( - keys: (keyof SessionDefaults)[], - args: Record, -): string[] { - return keys.filter((k) => args[k] == null && sessionStore.get(k) == null); -} - -export function createSessionAwareTool(opts: { - internalSchema: z.ZodType; - logicFunction: (params: TParams, executor: CommandExecutor) => Promise; - getExecutor: () => CommandExecutor; - requirements?: SessionRequirement[]; // preflight, friendlier than raw zod errors -}) { - const { internalSchema, logicFunction, getExecutor, requirements = [] } = opts; - - return async (rawArgs: Record): Promise => { - try { - // Merge: explicit args take precedence over session defaults - const merged: Record = { ...sessionStore.getAll(), ...rawArgs }; - - // Preflight requirement checks (clear message how to fix) - for (const req of requirements) { - if ('allOf' in req) { - const missing = missingFromArgsAndSession(req.allOf, rawArgs); - if (missing.length > 0) { - return createErrorResponse( - 'Missing required session defaults', - `${req.message ?? `Required: ${req.allOf.join(', ')}`}\n` + - `Set with: session-set-defaults { ${missing.map((k) => `"${k}": "..."`).join(', ')} }`, - ); - } - } else if ('oneOf' in req) { - const missing = missingFromArgsAndSession(req.oneOf, rawArgs); - // oneOf satisfied if at least one is present in merged - const satisfied = req.oneOf.some((k) => merged[k] != null); - if (!satisfied) { - return createErrorResponse( - 'Missing required session defaults', - `${req.message ?? `Provide one of: ${req.oneOf.join(', ')}`}\n` + - `Set with: session-set-defaults { "${req.oneOf[0]}": "..." }`, - ); - } - } - } - - // Validate against unchanged internal schema (logic/api untouched) - const validated = internalSchema.parse(merged); - return await logicFunction(validated, getExecutor()); - } catch (error) { - if (error instanceof z.ZodError) { - const msgs = error.errors.map((e) => `${e.path.join('.') || 'root'}: ${e.message}`); - return createErrorResponse( - 'Parameter validation failed', - `Invalid parameters:\n${msgs.join('\n')}\n` + - `Tip: set session defaults via session-set-defaults`, - ); - } - throw error; - } - }; -} -``` - -## Plugin Migration Pattern (Example: build_sim) - -Public schema hides session fields; handler uses session-aware factory with internal schema and requirements; logic function unchanged. - -```typescript -// src/mcp/tools/simulator/build_sim.ts (key parts only) -import { z } from 'zod'; -import { createSessionAwareTool } from '../../../utils/typed-tool-factory.ts'; -import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; - -// Existing internal schema (unchanged)… -const baseOptions = { /* as-is (scheme, simulatorId, simulatorName, configuration, …) */ }; -const baseSchemaObject = z.object({ - projectPath: z.string().optional(), - workspacePath: z.string().optional(), - ...baseOptions, -}); -const baseSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject); -const buildSimulatorSchema = baseSchema - .refine(/* as-is: projectPath XOR workspacePath */) - .refine(/* as-is: simulatorId XOR simulatorName */); - -export type BuildSimulatorParams = z.infer; - -// Public schema = internal minus session-managed fields -const sessionManaged = [ - 'projectPath', - 'workspacePath', - 'scheme', - 'configuration', - 'simulatorId', - 'simulatorName', - 'useLatestOS', -] as const; - -const publicSchemaObject = baseSchemaObject.omit( - Object.fromEntries(sessionManaged.map((k) => [k, true])) as Record, -); - -export default { - name: 'build_sim', - description: 'Builds an app for an iOS simulator.', - schema: publicSchemaObject.shape, // what the MCP client sees - handler: createSessionAwareTool({ - internalSchema: buildSimulatorSchema, - logicFunction: build_simLogic, - getExecutor: getDefaultCommandExecutor, - requirements: [ - { allOf: ['scheme'], message: 'scheme is required' }, - { oneOf: ['projectPath', 'workspacePath'], message: 'Provide a project or workspace' }, - { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, - ], - }), -}; -``` - -This same pattern applies to `build_run_sim`, `test_sim`, device/macos tools, etc. Public schemas become minimal, while internal schemas and logic remain unchanged. - -## New Tool Group: session-management - -### session_set_defaults.ts - -```typescript -// src/mcp/tools/session-management/session_set_defaults.ts -import { z } from 'zod'; -import { sessionStore, type SessionDefaults } from '../../../utils/session-store.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; -import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; - -const schemaObj = z.object({ - projectPath: z.string().optional(), - workspacePath: z.string().optional(), - scheme: z.string().optional(), - configuration: z.string().optional(), - simulatorName: z.string().optional(), - simulatorId: z.string().optional(), - deviceId: z.string().optional(), - useLatestOS: z.boolean().optional(), - arch: z.enum(['arm64', 'x86_64']).optional(), -}); -type Params = z.infer; - -async function logic(params: Params): Promise { - sessionStore.setDefaults(params as Partial); - const current = sessionStore.getAll(); - return { content: [{ type: 'text', text: `Defaults updated:\n${JSON.stringify(current, null, 2)}` }] }; -} - -export default { - name: 'session-set-defaults', - description: 'Set session defaults used by other tools.', - schema: schemaObj.shape, - handler: createTypedTool(schemaObj, logic, getDefaultCommandExecutor), -}; -``` - -### session_clear_defaults.ts - -```typescript -// src/mcp/tools/session-management/session_clear_defaults.ts -import { z } from 'zod'; -import { sessionStore } from '../../../utils/session-store.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; -import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; - -const keys = [ - 'projectPath','workspacePath','scheme','configuration', - 'simulatorName','simulatorId','deviceId','useLatestOS','arch', -] as const; -const schemaObj = z.object({ - keys: z.array(z.enum(keys)).optional(), - all: z.boolean().optional(), -}); - -async function logic(params: z.infer) { - if (params.all || !params.keys) sessionStore.clear(); - else sessionStore.clear(params.keys); - return { content: [{ type: 'text', text: 'Session defaults cleared' }] }; -} - -export default { - name: 'session-clear-defaults', - description: 'Clear selected or all session defaults.', - schema: schemaObj.shape, - handler: createTypedTool(schemaObj, logic, getDefaultCommandExecutor), -}; -``` - -### session_show_defaults.ts - -```typescript -// src/mcp/tools/session-management/session_show_defaults.ts -import { sessionStore } from '../../../utils/session-store.ts'; - -export default { - name: 'session-show-defaults', - description: 'Show current session defaults.', - schema: {}, // no args - handler: async () => { - const current = sessionStore.getAll(); - return { content: [{ type: 'text', text: JSON.stringify(current, null, 2) }] }; - }, -}; -``` - -## Step-by-Step Implementation Plan (Incremental, buildable at each step) - -1. **Add SessionStore** ✅ **DONE** - - New file: `src/utils/session-store.ts`. - - No existing code changes; run: `npm run build`, `lint`, `test`. - - Commit checkpoint (after review): see Commit & Review Protocol below. - -2. **Add session-management tools** ✅ **DONE** - - New folder: `src/mcp/tools/session-management` with the three tools above. - - Register via existing plugin discovery (same pattern as others). - - Build and test. - - Commit checkpoint (after review). - -3. **Add session-aware tool factory** ✅ **DONE** - - Add `createSessionAwareTool` to `src/utils/typed-tool-factory.ts` (keep `createTypedTool` intact). - - Unit tests for requirement preflight and merge precedence. - - Commit checkpoint (after review). - -4. **Migrate 2-3 representative tools** - - Example: `simulator/build_sim`, `macos/build_macos`, `device/build_device`. - - Create `publicSchemaObject` (omit session fields), switch handler to `createSessionAwareTool` with requirements. - - Keep internal schema and logic unchanged. Build and test. - - Commit checkpoint (after review). - -5. **Migrate remaining tools in small batches** - - Apply the same pattern across simulator/device/macos/test utilities. - - After each batch: `npm run typecheck`, `lint`, `test`. - - Commit checkpoint (after review). - -6. **Final polish** - - Add tests for session tools and session-aware preflight error messages. - - Ensure public schemas no longer expose session parameters globally. - - Commit checkpoint (after review). - -## Standard Testing & DI Checklist (Mandatory) - -- Handlers must use dependency injection; tests must never call real executors. -- For validation-only tests, calling the handler is acceptable because Zod validation occurs before executor acquisition. -- For logic tests that would otherwise trigger `getDefaultCommandExecutor`, export the logic function and test it directly (no executor needed if logic doesn’t use one): - -```ts -// Example: src/mcp/tools/session-management/session_clear_defaults.ts -export async function sessionClearDefaultsLogic(params: Params): Promise { /* ... */ } -export default { - name: 'session-clear-defaults', - handler: createTypedTool(schemaObj, sessionClearDefaultsLogic, getDefaultCommandExecutor), -}; - -// Test: import logic and call directly to avoid real executor -import plugin, { sessionClearDefaultsLogic } from '../session_clear_defaults.ts'; -``` - -- Add tests for the new group and tools: - - Group metadata test: `src/mcp/tools/session-management/__tests__/index.test.ts` - - Tool tests: `session_set_defaults.test.ts`, `session_clear_defaults.test.ts`, `session_show_defaults.test.ts` - - Utils tests: `src/utils/__tests__/session-store.test.ts` - - Factory tests: `src/utils/__tests__/session-aware-tool-factory.test.ts` covering: - - Preflight requirements (allOf/oneOf) - - Merge precedence (explicit args override session defaults) - - Zod error reporting with helpful tips - -- Always run locally before requesting review: - - `npm run typecheck` - - `npm run lint` - - `npm run format:check` - - `npm run build` - - `npm run test` - - Perform a quick manual CLI check (mcpli or reloaderoo) per the Manual Testing section - -### Minimal Changes Policy for Tests (Enforced) - -- Only make material, essential edits to tests required by the code change (e.g., new preflight error messages or added/removed fields). -- Do not change sample input values or defaults in tests (e.g., flipping a boolean like `preferXcodebuild`) unless strictly necessary to validate behavior. -- Preserve the original intent and coverage of logic-function tests; keep handler vs logic boundaries intact. -- When session-awareness is added, prefer setting/clearing session defaults around tests rather than altering existing assertions or sample inputs. - -### Tool Description Policy (Enforced) - -- Keep tool descriptions concise (maximum one short sentence). -- Do not mention session defaults, setup steps, examples, or parameter relationships in descriptions. -- Use clear, imperative phrasing (e.g., "Builds an app for an iOS simulator."). -- Apply consistently across all migrated tools; update any tests that assert `description` to match the concise string only. - -## Commit & Review Protocol (Enforced) - -At the end of each numbered step above: - -1. Ensure all checks pass: `typecheck`, `lint`, `format:check`, `build`, `test`; then perform a quick manual CLI test (mcpli or reloaderoo) per the Manual Testing section. - - Verify tool descriptions comply with the Tool Description Policy (concise, no session-defaults mention). -2. Stage only the files for that step. -3. Prepare a concise commit message focused on the “why”. -4. Request manual review and approval before committing. Do not push. - -Example messages per step: - -- Step 1 (SessionStore) - - `chore(utils): add in-memory SessionStore for session defaults` - - Body: “Introduces singleton SessionStore with set/get/clear/show for session defaults; no behavior changes.” - -- Step 2 (session-management tools) - - `feat(session-management): add set/clear/show session defaults tools and workflow metadata` - - Body: “Adds tools to manage session defaults and exposes workflow metadata; minimal schemas via typed factory.” - -- Step 3 (middleware) - - `feat(utils): add createSessionAwareTool with preflight requirements and args>session merge` - - Body: “Session-aware interop layer performing requirements checks and Zod validation against internal schema.” - -- Step 6 (tests/final polish) - - `test(session-management): add tool, store, and middleware tests; export logic for DI` - - Body: “Covers group metadata, tools, SessionStore, and factory (requirements/merge/errors). No production behavior changes.” - -Approval flow: -- After preparing messages and confirming checks, request maintainer approval. -- On approval: commit locally (no push). -- On rejection: revise and re-run checks. - -Note on commit hooks and selective commits: -- The pre-commit hook runs format/lint/build and can auto-add or modify files, causing additional files to be included in the commit. If you must commit a minimal subset, skip hooks with: `git commit --no-verify` (use sparingly and run `npm run typecheck && npm run lint && npm run test` manually first). - -## Safety, Buildability, Testability - -- Logic functions and their types remain unchanged; existing unit tests that import logic directly continue to pass. -- Public schemas shrink; MCP clients see smaller input schemas without session fields. -- Handlers validate with internal schemas after session-defaults merge, preserving runtime guarantees. -- Preflight requirement checks return clear guidance, e.g., "Provide one of: projectPath or workspacePath" + "Set with: session-set-defaults { "projectPath": "..." }". - -## Developer Usage - -- **Set defaults once**: - - `session-set-defaults { "workspacePath": "...", "scheme": "App", "simulatorName": "iPhone 16" }` -- **Run tools without args**: - - `build_sim {}` -- **Inspect/reset**: - - `session-show-defaults {}` - - `session-clear-defaults { "all": true }` - -## Manual Testing with mcpli (CLI) - -The following commands exercise the session workflow end‑to‑end using the built server. - -1) Build the server (required after code changes): - -```bash -npm run build -``` - -2) Discover a scheme (optional helper): - -```bash -mcpli --raw list-schemes --projectPath "/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj" -- node build/cli.js mcp -``` - -3) Set the session defaults (project/workspace, scheme, and simulator): - -```bash -mcpli --raw session-set-defaults \ - --projectPath "/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj" \ - --scheme MCPTest \ - --simulatorName "iPhone 16" \ - -- node build/cli.js mcp -``` - -4) Verify defaults are stored: - -```bash -mcpli --raw session-show-defaults -- node build/cli.js mcp -``` - -5) Run a session‑aware tool with zero or minimal args (defaults are merged automatically): - -```bash -# Optionally provide a scratch derived data path and a short timeout -mcpli --tool-timeout=60 --raw build-sim --derivedDataPath "/tmp/XBMCP_DD" -- node build/cli.js mcp -``` - -Troubleshooting: - -- If you see validation errors like “Missing required session defaults …”, (re)run step 3 with the missing keys. -- If you see connect ECONNREFUSED or the daemon appears flaky: - - Check logs: `mcpli daemon log --since=10m -- node build/cli.js mcp` - - Restart daemon: `mcpli daemon restart -- node build/cli.js mcp` - - Clean daemon state: `mcpli daemon clean -- node build/cli.js mcp` then `mcpli daemon start -- node build/cli.js mcp` - - After code changes, always: `npm run build` then `mcpli daemon restart -- node build/cli.js mcp` - -Notes: - -- Public schemas for session‑aware tools intentionally omit session fields (e.g., `scheme`, `projectPath`, `simulatorName`). Provide them once via `session-set-defaults` and then call the tool with zero/minimal flags. -- Use `--tool-timeout=` to cap long‑running builds during manual testing. -- mcpli CLI normalizes tool names: tools exported with underscores (e.g., `build_sim`) can be invoked with hyphens (e.g., `build-sim`). Copy/paste samples using hyphens are valid because mcpli converts underscores to dashes. - -## Next Steps - -Would you like me to proceed with Phase 1–3 implementation (store + session tools + middleware), then migrate a first tool (build_sim) and run the test suite? diff --git a/docs/dev/tools_cli_schema_audit_plan.md b/docs/dev/tools_cli_schema_audit_plan.md deleted file mode 100644 index 2065e6f9..00000000 --- a/docs/dev/tools_cli_schema_audit_plan.md +++ /dev/null @@ -1,100 +0,0 @@ -# Tools CLI Schema Audit Output Plan - -Goal: add a new Tools CLI option that prints every tool name, tool description, and tool call arguments with their descriptions (when set), plus a `--json` version, to audit tool schema strings. - -## Current State (What Exists) -- CLI entrypoint: `scripts/tools-cli.ts` handles `count`, `list`, `static` and `--json`. -- Static analysis: `scripts/analysis/tools-analysis.ts` parses tool descriptions via AST but does not parse argument schemas. -- Runtime tool registration uses Zod schema shapes: `src/core/plugin-types.ts` + `src/utils/tool-registry.ts`. -- Tool schemas are Zod shapes with optional `.describe()` strings (examples in `src/mcp/tools/**`). - -## Proposed CLI UX -- Add a new command `schema` (alias `schemas` or `audit`) or a new flag `--schema` on `list`. -- Output (human-readable): - - Tool name - - Tool description - - Arguments section: - - Each arg name - - Arg description (or `No description provided`) -- Output (JSON): - - Array of tools with `name`, `description`, `args: [{ name, description }]` - - Keep the current `--json` behavior for other commands unchanged. - -## Data Source Decision -Prefer static runtime-free loading via generated workflow loaders: -- Load tools via `loadWorkflowGroups()` from `src/core/plugin-registry.ts` to access in-repo tool metadata without requiring a build or running server. -- Each tool already exposes `schema` as a Zod shape (`Record`). -- Extract descriptions from Zod internals: `schema[key]._def.description` (guarded for undefined). - -Fallback considerations: -- Reloaderoo `list-tools` does not appear to include input schema; runtime inspection likely cannot provide argument descriptions without adding new server output. -- Keep the new audit option in “static” mode only to avoid build/runtime coupling. - -## Output Format (Human) -Example structure: - -``` -Tool: build_sim -Description: Builds an app for an iOS simulator. -Arguments: - - scheme: The scheme to use (Required) - - simulatorId: UUID of the simulator ... - - simulatorName: Name of the simulator ... - - configuration: Build configuration (Debug, Release, etc.) -``` - -Design notes: -- Sort tools alphabetically. -- Sort argument names alphabetically (or preserve schema insertion order for stability). -- Use clear labels and spacing; one blank line between tools. - -## JSON Format -Example: - -```json -{ - "tools": [ - { - "name": "build_sim", - "description": "Builds an app for an iOS simulator.", - "args": [ - { "name": "scheme", "description": "The scheme to use (Required)" }, - { "name": "simulatorId", "description": "UUID of the simulator ..." } - ] - } - ] -} -``` - -Keep `--json` aligned with existing behavior: if the new command is used, emit only the schema-audit JSON; do not include summary stats unless explicitly requested. - -## Implementation Steps (Do Not Execute) -1. Add CLI command/flag parsing in `scripts/tools-cli.ts` for the schema audit view; update help text. -2. Add a static loader path: - - Import `loadWorkflowGroups()` from `src/core/plugin-registry.ts`. - - Gather all tools, dedupe by tool name (match `tool-registry` behavior). -3. Extract argument descriptions: - - For each tool’s `schema` (shape), iterate entries. - - Pull `description` from `schemaEntry?._def?.description` (string or undefined). - - Produce `{ name, description }` records; use `null` or “No description provided” in human output. -4. Implement output formatting: - - Human-readable: labeled fields per tool. - - JSON: `tools: []` only (no emojis, no ANSI). -5. Add tests: - - Unit test to ensure a tool with Zod `.describe()` surfaces descriptions. - - Test for tools with no arg descriptions (ensure output uses fallback). - - Snapshot-style test for the JSON schema output. -6. Docs: - - Update CLI help text in `scripts/tools-cli.ts`. - - If this is considered a tooling change, consider updating CLI docs via `npm run docs:update` (per `docs/dev/README.md` conventions). - -## Risks and Edge Cases -- Some tools use session-aware public schemas; descriptions may be absent for session-managed args (expected). -- Zod `.describe()` is optional; audit must handle missing descriptions without errors. -- Dynamic imports in `loadWorkflowGroups()` may require tsx execution (already used by tools CLI). - -## Verification Plan (When Implementing) -- Run `npm run typecheck`, `npm run lint`, `npm run format:check`, `npm run test`. -- Manual sanity: - - `npm run tools schema` - - `npm run tools schema --json` diff --git a/docs/dev/tools_schema_redundancy.md b/docs/dev/tools_schema_redundancy.md deleted file mode 100644 index 83828acb..00000000 --- a/docs/dev/tools_schema_redundancy.md +++ /dev/null @@ -1,63 +0,0 @@ -# Tools Schema Session-Default Audit - -This document tracks session-default migrations that remove per-tool arguments from schemas when they are typically set once per session. - -## Session defaults to add or reinforce - -- [x] derivedDataPath: Added to session defaults; removed from build/test/clean tool schemas. -- [x] preferXcodebuild: Added to session defaults; removed from build/test/clean tool schemas. -- [x] configuration (SwiftPM + Xcode): Session default described as applying to SwiftPM and Xcode tools. -- [x] platform: Added to session defaults; applies to device-only tools (get_device_app_path, test_device). -- [x] bundleId: Added to session defaults; applies to launch/stop/log tools for single-app workflows. - -## Removal checklist by tool - -### Build and test (Xcode) - -Remove derivedDataPath, preferXcodebuild from: - -- [x] build_device -- [x] build_sim -- [x] build_run_sim -- [x] build_macos -- [x] build_run_macos -- [x] test_device -- [x] test_sim -- [x] test_macos -- [x] clean - -Remove platform from: - -- [x] get_device_app_path -- [x] test_device - -### Swift Package Manager - -Remove configuration from: - -- [x] swift_package_build -- [x] swift_package_run -- [x] swift_package_test - -### Launch/stop/log (bundleId default) - -Remove bundleId from: - -- [x] launch_app_device -- [x] launch_app_sim -- [x] launch_app_logs_sim -- [x] stop_app_sim -- [x] start_device_log_cap -- [x] start_sim_log_cap - -## Non-candidates (keep in schemas) - -- extraArgs: Single-use per invocation; keep per tool. -- appPath: Depends on build output or target app; varies between calls. -- args (launch_app_*): Varies per launch. -- UI interaction coordinates and durations (tap/swipe/gesture): Always call-specific. - -## Description updates - -- [x] session-set-defaults configuration description clarifies SwiftPM + Xcode usage. -- [x] session-set-defaults platform description clarifies device-only usage. diff --git a/docs/investigations/INVESTIGATION_TOOL_DISCOVERY.md b/docs/investigations/INVESTIGATION_TOOL_DISCOVERY.md deleted file mode 100644 index bf3154de..00000000 --- a/docs/investigations/INVESTIGATION_TOOL_DISCOVERY.md +++ /dev/null @@ -1,54 +0,0 @@ -# Investigation: Workflow/Tool Discovery, Registration, and Visibility (CLI vs MCP) - -## Summary -Discovery is build-time (filesystem -> generated loaders), with runtime selection/visibility filtering applied in CLI/MCP/daemon catalogs. CLI `tools` listing diverges because it reads a static manifest that bypasses runtime visibility and naming rules. - -## Symptoms -- CLI `tools` output can list tools that runtime CLI commands do not expose (debug-gated tools). -- Tool names in the manifest can differ from actual MCP tool names when export `name` differs from filename. - -## Investigation Log - -### 2026-02-04 - Build-time discovery and codegen -**Hypothesis:** Tools/workflows are discovered at build-time from filesystem and compiled into generated loaders. -**Findings:** Discovery scans `src/mcp/tools/*` and generates loaders/metadata in `src/core/generated-plugins.ts`, which runtime uses for plugin loading. -**Evidence:** `build-plugins/plugin-discovery.ts`, `src/core/generated-plugins.ts`, `src/core/plugin-registry.ts`. -**Conclusion:** Confirmed. Runtime does not live-scan the filesystem; it depends on generated loaders. - -### 2026-02-04 - Runtime catalog and visibility filtering -**Hypothesis:** Runtime catalogs apply workflow selection and tool visibility filters. -**Findings:** `buildToolCatalog()` filters tools through `shouldExposeTool()` and resolves workflow selection, then CLI registers tool commands from the catalog. -**Evidence:** `src/runtime/tool-catalog.ts`, `src/utils/tool-visibility.ts`, `src/utils/workflow-selection.ts`, `src/cli/cli-tool-catalog.ts`, `src/cli/register-tool-commands.ts`. -**Conclusion:** Confirmed. Runtime catalog is the source for actual CLI commands, not the manifest. - -### 2026-02-04 - CLI `tools` list divergence -**Hypothesis:** `xcodebuildmcp tools` uses a static manifest and ignores runtime visibility gates. -**Findings:** `tools` command loads `tools-manifest.json` and filters only by workflow exclusion/CLI flags; it does not call `buildToolCatalog()` or `shouldExposeTool()`. -**Evidence:** `src/cli/commands/tools.ts`, `scripts/generate-tools-manifest.ts`, `scripts/analysis/tools-analysis.ts`. -**Conclusion:** Confirmed. CLI `tools` can disagree with runtime command availability, especially for debug-gated tools. - -### 2026-02-04 - Tool name mismatch (manifest vs runtime) -**Hypothesis:** Manifest uses filename as tool name while runtime uses export `name`, causing mismatches. -**Findings:** Manifest derives `ToolInfo.name` from file basename and writes `mcpName` from that; runtime uses `export default { name: ... }` as tool identity. -**Evidence:** `scripts/analysis/tools-analysis.ts`, `scripts/generate-tools-manifest.ts`, `src/core/plugin-registry.ts`, `src/mcp/tools/workflow-discovery/manage_workflows.ts`. -**Conclusion:** Confirmed. Example mismatch: `manage_workflows.ts` exports `name: 'manage-workflows'` but manifest reports `manage_workflows`. - -### 2026-02-04 - Recent history review -**Hypothesis:** Recent commits may have introduced or modified these discovery/visibility flows. -**Findings:** Recent commits include MCP tool support and tool analysis updates around 2026-02-04. -**Evidence:** `git log -n 20` (commit `df690484` and `f4604d65`). -**Conclusion:** The MCP additions and tool analysis updates are recent; they likely interact with the manifest/runtime divergence. - -## Root Cause -1) **CLI `tools` listing bypasses runtime catalog and visibility gates.** It reads a static manifest file and does not consult `shouldExposeTool()` or disambiguated CLI names, so it can show tools not available at runtime. -2) **Manifest naming uses filename as canonical tool identity.** Runtime identity is the exported `name` field, so any filename/name mismatch causes the manifest (and CLI listing/docs) to diverge from actual MCP tool names. - -## Recommendations -1. Align `xcodebuildmcp tools` with the runtime catalog output (or apply the same visibility gates and naming rules in manifest generation). -2. Decide on a single source-of-truth for tool names: - - Enforce `filename === export name`, or - - Parse `name` from the default export during manifest generation. - -## Preventive Measures -- Add a build-time validation step that fails when tool filenames and exported names diverge. -- Add a test that compares manifest output with runtime catalog to catch visibility/name drift. diff --git a/docs/investigations/daemon-log-missing.md b/docs/investigations/daemon-log-missing.md deleted file mode 100644 index 63b8dce0..00000000 --- a/docs/investigations/daemon-log-missing.md +++ /dev/null @@ -1,34 +0,0 @@ -# Investigation: Daemon Log File Missing Entries - -## Summary -Daemon log files only contained the init line because daemon start used the global CLI `--log-level` default (`none`), which was unintentionally passed through to the daemon. That set `clientLogLevel` to `none`, suppressing file writes in `log()`. - -## Symptoms -- Daemon log file existed but only contained “Log file initialized”. -- Foreground daemon printed logs to console, but file didn’t show them. - -## Investigation Log - -### 2026-02-02 - CLI Argument Collision -**Hypothesis:** Daemon log level was being set to `none` inadvertently. -**Findings:** `src/cli/yargs-app.ts` defines global `--log-level` default `none`. `src/cli/commands/daemon.ts` read the same flag and forwarded it to `XCODEBUILDMCP_DAEMON_LOG_LEVEL`, so daemon started with log level `none` unless explicitly overridden. -**Evidence:** `src/cli/yargs-app.ts`, `src/cli/commands/daemon.ts` -**Conclusion:** Confirmed. This explains “init line only” behavior. - -### 2026-02-02 - Logger File Guard -**Hypothesis:** File logging is suppressed when `clientLogLevel === 'none'`. -**Findings:** `log()` writes to file only when `logFileStream` exists and `clientLogLevel !== 'none'`, while `setLogFile()` writes the init line unconditionally. -**Evidence:** `src/utils/logger.ts` -**Conclusion:** Confirmed. This is why the file has only the init line. - -## Root Cause -Daemon CLI reused the global `--log-level` option (default `none`) for daemon log level, which set `XCODEBUILDMCP_DAEMON_LOG_LEVEL=none` during daemon start. The logger then skipped all file writes after initialization. - -## Recommendations -1. Use distinct daemon flags (`--daemon-log-level`, `--daemon-log-path`) to avoid collision. -2. Log daemon startup errors via `log()` so they appear in the daemon log file. -3. Keep daemon startup logs after log file setup to ensure they are captured. - -## Preventive Measures -- Avoid reusing global CLI flags for subsystem-specific settings. -- Treat `none` as “stderr only” for CLI but keep file logging explicitly controlled to avoid accidental suppression. \ No newline at end of file diff --git a/docs/investigations/issue-154-screenshot-downscaling.md b/docs/investigations/issue-154-screenshot-downscaling.md deleted file mode 100644 index c38e4464..00000000 --- a/docs/investigations/issue-154-screenshot-downscaling.md +++ /dev/null @@ -1,44 +0,0 @@ -# Investigation: Optional Screenshot Downscaling (Issue #154) - -## Summary -Investigation started; initial context gathered from the issue description. Context builder failed (Gemini CLI usage error), so manual exploration is proceeding. - -## Symptoms -- Screenshots captured for UI automation are full-resolution by default. -- High-resolution screenshots increase multimodal token usage and cost. - -## Investigation Log - -### 2026-01-04 - Initial assessment -**Hypothesis:** Screenshot pipeline always emits full-resolution images and lacks an opt-in scaling path. -**Findings:** Issue describes full-res screenshots and requests optional downscaling. No code inspected yet. -**Evidence:** GitHub issue #154 body. -**Conclusion:** Needs codebase investigation. - -### 2026-01-04 - Context builder attempt -**Hypothesis:** Use automated context discovery to map screenshot capture flow. -**Findings:** `context_builder` failed due to Gemini CLI usage error in this environment. -**Evidence:** Tool error output in session (Gemini CLI usage/help text). -**Conclusion:** Proceeding with manual code inspection. - -### 2026-01-04 - Screenshot capture implementation -**Hypothesis:** Screenshot tool stores and returns full-resolution PNGs. -**Findings:** The `screenshot` tool captures a PNG, then immediately downscales/optimizes via `sips` to max 800px width, JPEG format, quality 75%, and returns the JPEG. Optimization is always attempted; on failure it falls back to original PNG. -**Evidence:** `src/mcp/tools/ui-testing/screenshot.ts` (sips `-Z 800`, `format jpeg`, `formatOptions 75`). -**Conclusion:** The current implementation already downscales by default; the gap is configurability (opt-in/out, size/quality controls) and documentation. - -### 2026-01-04 - Git history check -**Hypothesis:** Recent commits might have added/changed screenshot optimization behavior. -**Findings:** Recent history shows tool annotations and session-awareness changes, but no indication of configurable screenshot scaling. -**Evidence:** `git log -n 5 -- src/mcp/tools/ui-testing/screenshot.ts`. -**Conclusion:** No recent change introduces optional scaling controls. - -## Root Cause -The issue report assumes full-resolution screenshots, but the current `screenshot` tool already downsamples to 800px max width and JPEG 75% every time. There is no parameter to disable or tune this behavior, and docs do not mention the optimization. - -## Recommendations -1. Document existing downscaling behavior and defaults in tool docs (and in the screenshot tool description). -2. Add optional parameters to `screenshot` for max width/quality/format or a boolean to disable optimization, preserving current defaults. - -## Preventive Measures -- Add a section in docs/TOOLS.md or tool-specific docs describing image processing defaults and token tradeoffs. diff --git a/docs/investigations/issue-debugger-attach-stopped.md b/docs/investigations/issue-debugger-attach-stopped.md deleted file mode 100644 index df3e31ac..00000000 --- a/docs/investigations/issue-debugger-attach-stopped.md +++ /dev/null @@ -1,38 +0,0 @@ -# Investigation: Debugger attaches in stopped state after launch - -## Summary -Reproduced: attaching the debugger leaves the simulator app in a stopped state. UI automation is blocked by the guard because the debugger reports `state=stopped`. The attach flow does not issue any resume/continue, so the process remains paused after attach. - -## Symptoms -- After attaching debugger to Calculator, UI automation taps fail because the app is paused. -- UI guard blocks with `state=stopped` immediately after attach. - -## Investigation Log - -### 2025-02-14 - Repro (CalculatorApp on iPhone 17 simulator) -**Hypothesis:** Attach leaves the process stopped, which triggers the UI automation guard. -**Findings:** `debug_attach_sim` attached to a running CalculatorApp (DAP backend), then `tap` was blocked with `state=stopped`. -**Evidence:** `tap` returned "UI automation blocked: app is paused in debugger" with `state=stopped` and the current debug session ID. -**Conclusion:** Confirmed. - -### 2025-02-14 - Code Review (attach flow) -**Hypothesis:** The attach implementation does not resume the process. -**Findings:** The attach flow never calls any resume/continue primitive. -- `debug_attach_sim` creates a session and returns without resuming. -- DAP backend attach flow (`initialize -> attach -> configurationDone`) has no `continue`. -- LLDB CLI backend uses `process attach --pid` and never `process continue`. -- UI automation guard blocks when state is `stopped`. -**Evidence:** `src/mcp/tools/debugging/debug_attach_sim.ts`, `src/utils/debugger/backends/dap-backend.ts`, `src/utils/debugger/backends/lldb-cli-backend.ts`, `src/utils/debugger/ui-automation-guard.ts`. -**Conclusion:** Confirmed. Stopped state originates from debugger attach semantics, and the tool never resumes. - -## Root Cause -The debugger attach path halts the target process (standard debugger behavior) and there is no subsequent resume/continue step. This leaves the process in `stopped` state, which causes `guardUiAutomationAgainstStoppedDebugger` to block UI tools like `tap`. - -## Recommendations -1. Add a first-class `debug_continue` tool backed by a backend-level `continue()` API to resume without relying on LLDB command evaluation. -2. Add an optional `continueOnAttach` (or `stopOnAttach`) parameter to `debug_attach_sim`, with a default suited for UI automation workflows. -3. Update guard messaging to recommend `debug_continue` (not `debug_lldb_command continue`, which is unreliable on DAP). - -## Preventive Measures -- Document that UI tools require the target process to be running, and that debugger attach may pause execution by default. -- Add a state check or auto-resume option when attaching in automation contexts. diff --git a/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md b/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md deleted file mode 100644 index 85d85eb1..00000000 --- a/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md +++ /dev/null @@ -1,53 +0,0 @@ -# RCA: snapshot_ui returns empty tree after debugger resume - -## Summary -When the app is stopped under LLDB (breakpoints hit), the `snapshot_ui` tool frequently returns an empty accessibility tree (0x0 frame, no children). This is not because of a short timing gap after resume. The root cause is that the process is still stopped (or immediately re-stopped) due to active breakpoints, so AX snapshotting cannot retrieve a live hierarchy. - -## Impact -- UI automation appears "broken" after resuming from breakpoints. -- Simulator UI may visually update only after detaching or clearing breakpoints because the process is repeatedly stopped. -- `snapshot_ui` can return misleading empty trees even though the app is running in the simulator. - -## Environment -- App: Calculator (example project) -- Simulator: iPhone 16 (2FCB5689-88F1-4CDF-9E7F-8E310CD41D72) -- Debug backend: LLDB CLI - -## Repro Steps -1. Attach debugger to the simulator app (`debug_attach_sim`). -2. Set breakpoint at `CalculatorButton.swift:18` and `CalculatorInputHandler.swift:12`. -3. `debug_lldb_command` -> `continue`. -4. Tap a button (e.g., "7") so breakpoints fire. -5. `debug_lldb_command` -> `continue`. -6. Call `snapshot_ui` immediately after resume. - -## Observations -- `debug_stack` immediately after resume shows stop reason `breakpoint 1.2` or `breakpoint 2.1`. -- Multiple `continue` calls quickly re-stop the process due to breakpoints in SwiftUI button handling and input processing. -- While stopped, `snapshot_ui` often returns: - - Application frame: `{{0,0},{0,0}}` - - `AXLabel` null - - No children -- Waiting does not help. We tested 1s, 2s, 3s, 5s, 8s, and 10s delays; the tree remained empty in a stopped state. -- Once breakpoints are removed and the process is running, `snapshot_ui` returns the full tree immediately. -- Detaching the debugger also restores `snapshot_ui` output. - -## Root Cause -The process is stopped due to breakpoints, or repeatedly re-stopped after resume. AX snapshots cannot read a paused process, so `snapshot_ui` returns an empty hierarchy. - -## Confirming Evidence -- `debug_stack` after `continue` shows: - - `stop reason = breakpoint 1.2` at `CalculatorButton.swift:18` - - `stop reason = breakpoint 2.1` at `CalculatorInputHandler.swift:12` -- After removing breakpoints and `continue`, `snapshot_ui` returns a full hierarchy (buttons + display values). - -## Current Workarounds -- Clear or remove breakpoints before calling `snapshot_ui`. -- Detach the debugger to allow the app to run normally. - -## Recommendations -- Document that `snapshot_ui` requires the target process to be running (not stopped under LLDB). -- Provide guidance to: - - Remove or disable breakpoints before UI automation. - - Avoid calling `snapshot_ui` immediately after breakpoints unless resumed and confirmed running. -- Optional future enhancement: add a tool-level warning when the debugger session is stopped, or add a helper command that validates "running" state before UI inspection. diff --git a/docs/investigations/launch-app-logs-sim.md b/docs/investigations/launch-app-logs-sim.md deleted file mode 100644 index 9becfed2..00000000 --- a/docs/investigations/launch-app-logs-sim.md +++ /dev/null @@ -1,39 +0,0 @@ -# Investigation: launch-app-logs-sim keeps CLI running - -## Summary -The CLI remains alive because `launch_app_logs_sim` starts long-running log capture processes and keeps open streams in the same Node process, and the tool is not marked `cli.stateful` so it does not route through the daemon. - -## Symptoms -- `node build/cli.js simulator launch-app-logs-sim --simulator-id B38FE93D-578B-454B-BE9A-C6FA0CE5F096 --bundle-id com.example.calculatorapp` keeps the CLI process running while the app is running. - -## Investigation Log - -### 2026-02-01 21:54 UTC - Initial context build -**Hypothesis:** The tool runs long-lived log streaming in the CLI process, preventing exit. -**Findings:** `launch_app_logs_sim` delegates to `startLogCapture`, which spawns long-running log processes and keeps a writable stream open. The tool is not marked `cli.stateful`, so it runs in-process. -**Evidence:** `src/mcp/tools/simulator/launch_app_logs_sim.ts`, `src/utils/log_capture.ts`, `src/utils/command.ts`, `src/runtime/tool-invoker.ts`, `src/runtime/tool-catalog.ts`. -**Conclusion:** Confirmed. - -### 2026-02-01 21:54 UTC - Git history review -**Hypothesis:** Recent changes introduced or reinforced CLI in-process routing for this tool. -**Findings:** Recent commit history shows the CLI was introduced on 2026-01-31 (“Make CLI”) and tool refactors/command executor changes occurred in late January. No commit message indicates lifecycle changes for log capture sessions. -**Evidence:** `git log -n 5 -- src/utils/log_capture.ts src/mcp/tools/simulator/launch_app_logs_sim.ts src/runtime/tool-invoker.ts`. -**Conclusion:** Inconclusive for intent; indicates the CLI implementation is recent and likely inherited default in-process routing. - -### 2026-02-01 21:54 UTC - Docs/tests intent check -**Hypothesis:** The tool is expected to return immediately, not block. -**Findings:** `launch_app_logs_sim` returns text instructing the user to interact, then stop capture later, and includes `nextSteps` for `stop_sim_log_cap`. The docs list the tool but do not state lifecycle behavior. -**Evidence:** `src/mcp/tools/simulator/launch_app_logs_sim.ts`, `src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts`, `docs/TOOLS.md`. -**Conclusion:** Weak but supportive signal for a non-blocking start/stop flow; docs are ambiguous. - -## Root Cause -`launch_app_logs_sim` starts log capture sessions that keep active event-loop handles (child process pipes and a log file stream). Because the tool lacks `cli.stateful: true`, the CLI invokes it in-process rather than routing through the daemon. The “detached” flag in `CommandExecutor` does not detach/unref the child, so the CLI cannot exit while capture is active. - -## Recommendations -1. Mark `launch_app_logs_sim`, `start_sim_log_cap`, and `stop_sim_log_cap` as `cli.stateful: true` so they run through the daemon and return promptly. -2. Clarify the `detached` flag semantics in `CommandExecutor` (rename or document) to avoid assuming it detaches the child process. -3. Document the lifecycle expectation for log-capture tools (start returns immediately; stop ends capture). - -## Preventive Measures -- Add explicit doc wording for stateful tools indicating daemon ownership and non-blocking behavior. -- Add tests or assertions that stateful tools are routed via daemon when invoked from CLI. diff --git a/docs/investigations/platform-inference-staleness-and-xor-normalization.md b/docs/investigations/platform-inference-staleness-and-xor-normalization.md deleted file mode 100644 index d6195033..00000000 --- a/docs/investigations/platform-inference-staleness-and-xor-normalization.md +++ /dev/null @@ -1,243 +0,0 @@ -# Investigation + Plan: Simulator Selector Normalization, CLI Determinism, and Platform Inference - -## Purpose - -This document separates three related but distinct problem domains and defines a concrete implementation plan for each. - -## Problem Domains - -1. Inconsistent handling of `simulatorId` and `simulatorName` across logic paths. -2. CLI hydrates session defaults, which makes CLI behavior non-deterministic. -3. Non-iOS simulator targets (watchOS/tvOS/visionOS) can use incorrect platform names and fail builds. - -## Scope - -1. Keep existing CLI argument UX and command surface. -2. Keep existing runtime validation behavior (`oneOf`/`allOf`/XOR checks). -3. Change only session-default hydration behavior, selector normalization behavior, and platform inference behavior. -4. Stateful CLI behavior (daemon, logs, debug sessions) remains by design and is out of scope. - -## Decision Snapshot - -1. Platform inference must support non-iOS simulators using simulator runtime metadata, with build-settings only as fallback. -2. Session defaults must be MCP-only runtime behavior (no CLI/daemon hydration into `sessionStore`). -3. Store both `simulatorId` and `simulatorName` in session defaults/config and disambiguate at tool boundary via shared helper logic. -4. Persist only `simulatorPlatform` as platform cache; do not persist `simulatorRuntime` or timestamp fields. - ---- - -## Domain 1: `simulatorId` / `simulatorName` Normalization - -## Current State (verified) - -1. `session_set_defaults` can keep both `simulatorId` and `simulatorName`. -2. Config normalization drops `simulatorName` when both are present. -3. Session-aware factory prunes exclusive pairs at merge-time and prefers first key when both come from defaults. -4. Some tools still enforce schema-level XOR, creating layered/duplicated enforcement. - -Net effect: behavior is usually correct, but inconsistent and hard to reason about. - -## Decision - -Use a single normalized model everywhere: - -1. Store both values in session defaults/config. -2. `simulatorId` is authoritative for tools that require UUID. -3. `simulatorName` is preserved for portability and tools that can use name. -4. Explicit user args that provide both remain invalid. -5. Each tool receives exactly one effective selector value at execution boundary. - -## Why this model - -1. `simulatorName` survives simulator resets better than UUIDs. -2. Some operations fundamentally require UUID; others can run with name. -3. Keeping both in storage avoids repeated lossy conversion and supports both execution modes. - -## Required Invariants - -1. Storage layer may contain both selector fields. -2. Explicit invocation args may not contain both selector fields. -3. Tool execution input must be disambiguated to one selector by a shared helper. -4. Disambiguation precedence must be deterministic and test-covered. - -## Implementation Plan - -1. Add `inferSimulatorSelectorForTool(...)` helper (or equivalent) used by simulator tools. -2. Normalize config/session behavior to stop dropping `simulatorName` when both exist. -3. Keep factory-level explicit XOR validation. -4. Keep per-tool requirement checks (`oneOf`/`allOf`) unchanged. -5. Reduce duplicated tool-local selector branching by centralizing selector choice. - -## Validation Plan - -1. Unit tests for helper precedence across explicit args, stored defaults, and missing values. -2. Regression tests for config-load behavior preserving both fields. -3. Integration tests for tools that require UUID vs tools that accept name. -4. Real-world MCP run validating both selector paths. - ---- - -## Domain 2: CLI Determinism and Session Defaults - -## Current State (verified) - -1. `session-management` workflow is not exposed in CLI. -2. CLI runtime still bootstraps config and hydrates `config.sessionDefaults` into `sessionStore`. -3. Result: CLI can pick up hidden persisted defaults that the user cannot inspect/mutate via CLI commands. - -Net effect: CLI behavior can vary based on hidden config state. - -## Decision - -Make session-default hydration MCP-only. - -1. MCP runtime hydrates `config.sessionDefaults` into `sessionStore`. -2. CLI and daemon runtimes do not hydrate `sessionDefaults` into `sessionStore`. -3. CLI/daemon still read non-session config (workflow filters, debug flags, timeouts, etc.). -4. No CLI command/flag redesign is required. - -## Required Invariants - -1. CLI tool behavior must not depend on persisted `sessionDefaults`. -2. CLI behavior remains explicit-argument driven. -3. Existing runtime validation for required parameters remains intact. -4. `disableSessionDefaults=true` behavior for MCP tools remains consistent with current expectations. - -## Implementation Plan - -1. Gate session-default hydration by runtime in `bootstrapRuntime`. -2. Ensure daemon startup path also does not hydrate selector defaults. -3. Add tests that verify: - - MCP hydrates session defaults. - - CLI and daemon do not. -4. Keep all existing CLI validation paths and error messages unless a bug is found. - -## Validation Plan - -1. Unit/integration tests for runtime hydration boundaries. -2. CLI real-world test with persisted `sessionDefaults` present in config: - - missing required args should still fail. - - explicit args should succeed. -3. MCP real-world test confirming session-default convenience still works. - ---- - -## Domain 3: Non-iOS Simulator Platform Inference - -## Current State (verified) - -1. iOS paths are generally reliable. -2. Non-iOS simulator targets can infer wrong platform and fail destination matching. -3. Build settings lookups are slower and should not be the first-line source for simulator platform inference. - -## Decision - -Platform inference for simulator tools must use simulator metadata first, then fallback. - -1. Primary source: simulator runtime metadata (via `simctl` resolution). -2. Derived output: correct simulator platform string (`iOS/watchOS/tvOS/visionOS Simulator`). -3. Secondary source: build settings only when simulator metadata cannot resolve. -4. Final fallback: explicit warning + `iOS Simulator` only when no better signal exists. - -Cache policy: - -1. Persist `simulatorPlatform` as the cached output. -2. Recompute `simulatorPlatform` during MCP startup hydration. -3. Recompute `simulatorPlatform` whenever simulator selector (`simulatorId`/`simulatorName`) changes. -4. Do not persist `simulatorRuntime` or timestamp fields. - -## Required Invariants - -1. If runtime indicates non-iOS simulator, platform must not default to iOS. -2. Platform inference source should be logged for observability. -3. Selector normalization and platform inference should be reusable utilities, not tool-local variants. - -## Implementation Plan - -1. Introduce/standardize `inferPlatform(...)` utility contract around selector + runtime metadata. -2. Ensure simulator-name and simulator-id paths both resolve runtime/platform deterministically. -3. Add normalized mapping from CoreSimulator runtime to xcodebuild destination platform. -4. Use build-settings only as fallback path. - -## Validation Plan - -1. Unit tests for runtime-to-platform mapping. -2. Integration tests for iOS + non-iOS simulator selectors. -3. Real-world CLI/MCP checks for watchOS/tvOS/visionOS flows where available. - ---- - -## Cross-Cutting Architecture - -## Shared Helpers - -1. `inferSimulatorSelectorForTool(...)` - - Input: explicit params + stored defaults + tool capability (`requiresId` vs `acceptsNameOrId`). - - Output: exactly one effective selector (or deterministic validation error). -2. `inferPlatform(...)` - - Input: resolved selector + scheme/path context. - - Output: `{ platform, source, runtime? }`. - -## Data Model - -Keep/add simulator metadata fields in session/config: - -1. `simulatorId` -2. `simulatorName` -3. `simulatorPlatform` (optional cache) - -`simulatorRuntime` can still be used transiently inside resolver/helper logic, but is not persisted. - -## Runtime Boundary Rules - -1. MCP: may hydrate/use session defaults. -2. CLI: no session-default hydration; explicit invocation only. -3. Daemon (CLI backend): same as CLI for hydration semantics. - ---- - -## Delivery Plan - -## Phase 0: Lock Decisions - -1. Approve this document’s three-domain decisions. -2. Confirm no CLI UX changes are in scope. - -## Phase 1: Runtime Boundary Fix (Domain 2) - -1. Implement MCP-only session-default hydration. -2. Add runtime boundary tests. - -## Phase 2: Selector Normalization (Domain 1) - -1. Implement shared selector helper. -2. Align config/session behavior to preserve both selector fields. -3. Remove path-specific inconsistencies. - -## Phase 3: Platform Inference Hardening (Domain 3) - -1. Consolidate non-iOS platform inference on simulator metadata. -2. Add mapping tests and fallback tests. - -## Phase 4: Regression + Real-World Validation - -1. Run project test/lint/typecheck/build pipeline. -2. Run real-world MCP and CLI smoke tests for selector + platform behavior. -3. Document outcomes in PR notes. - ---- - -## Risks and Mitigations - -1. Risk: duplicate validation behavior drifts across tools. - - Mitigation: central helper + contract tests. -2. Risk: config backward-compat surprises. - - Mitigation: additive fields and migration-safe parsing. -3. Risk: non-iOS paths regress silently. - - Mitigation: explicit non-iOS test coverage and runtime-source logging. - -## Out of Scope - -1. Redesigning CLI command structure or argument names. -2. Changing general daemon stateful behavior. -3. Introducing auto-retry heuristics on command failure. diff --git a/docs/investigations/spawn-sh-enoent.md b/docs/investigations/spawn-sh-enoent.md deleted file mode 100644 index 57b86d73..00000000 --- a/docs/investigations/spawn-sh-enoent.md +++ /dev/null @@ -1,59 +0,0 @@ -# Investigation: spawn sh ENOENT in build_sim/build_run_sim - -## Summary -Root cause is the command executor’s shell mode: it rewrites every command to `['sh','-c', ...]` and spawns `sh` by name, so any runtime with missing/empty PATH fails before xcodebuild runs. build_sim and build_run_sim both force or default to shell mode, so they fail immediately with `spawn sh ENOENT`. - -## Symptoms -- `build_run_sim` and `build_sim` fail immediately with `spawn sh ENOENT`. -- `doctor` reports PATH looks normal, but this likely reflects the CLI environment, not necessarily the daemon/runtime used for tool execution. -- Issue manifests on this branch; main reportedly unaffected. - -## Investigation Log - -### 2026-02-02 12:26:59 GMT - Phase 1/2 - Executor and build paths -**Hypothesis:** The failure is triggered by shell spawning inside the command executor. -**Findings:** -- `defaultExecutor` defaults `useShell = true` and rewrites commands to `['sh','-c', commandString]`, then `spawn`s the executable (`sh`). -- `executeXcodeBuildCommand` always passes `useShell = true` for xcodebuild, even though the command is already argv-style. -- `build_run_sim` executes `xcodebuild -showBuildSettings` with `useShell = true`, and most other commands omit `useShell`, inheriting the default `true`. -**Evidence:** -- `src/utils/command.ts:27-84` (default `useShell = true`, rewrites to `['sh','-c', ...]`, spawns executable) -- `src/utils/build-utils.ts:214-238` (xcodebuild executed with `useShell = true`) -- `src/mcp/tools/simulator/build_run_sim.ts:124-192` and `src/mcp/tools/simulator/build_run_sim.ts:244-303` (explicit `useShell = true` and default executor usage) -**Conclusion:** Confirmed. The error can be produced if PATH is missing/empty where executor runs. - -### 2026-02-02 12:26:59 GMT - Phase 3 - Env/daemon plumbing -**Hypothesis:** CLI/daemon code drops PATH or replaces env. -**Findings:** -- Daemon startup merges `process.env` with overrides; no PATH removal observed. -- Tool invoker only adds a few overrides (workflows/log level) and does not touch PATH. -**Evidence:** -- `src/cli/daemon-control.ts:23-114` (env merge preserves `process.env`) -- `src/runtime/tool-invoker.ts:64-142` (only XCODEBUILDMCP_* overrides) -**Conclusion:** Eliminated as direct cause. If PATH is missing, it originates in the host process environment. - -### 2026-02-02 12:26:59 GMT - Phase 4 - Other shell-dependent paths -**Hypothesis:** Incremental build path also requires shell. -**Findings:** -- xcodemake uses `getDefaultCommandExecutor()` without explicit `useShell` and runs `['which','xcodemake']`. -- `executeMakeCommand` uses `['cd', projectDir, '&&', 'make']`, which requires shell execution. -**Evidence:** -- `src/utils/xcodemake.ts:111-134` (which xcodemake uses default executor) -- `src/utils/xcodemake.ts:218-225` (`cd && make`) -**Conclusion:** Confirmed. This is a secondary path that would also fail under the same conditions. - -## Root Cause -The command execution layer always defaults to shell mode and shells are invoked by name (`sh`) via PATH lookup. This makes tool execution dependent on PATH-based resolution of `sh`. In the provided log, `spawn('sh', ...)` fails with `ENOENT` even though `process.env.PATH` includes `/bin`, so the failure is on resolving `sh` via PATH at spawn time. This implicates the `useShell = true` + `spawn('sh', ...)` design directly; the failure disappears if shell usage is removed or `/bin/sh` is used explicitly. - -## Recommendations -1. Use an absolute shell path when shelling out: replace `['sh','-c', ...]` with `['/bin/sh','-c', ...]` in `src/utils/command.ts`. This directly prevents `spawn sh ENOENT` and is the minimal hotfix. -2. Default to direct spawn (`useShell = false`) and only opt-in to shell when needed. Update call sites in: - - `src/utils/build-utils.ts` (xcodebuild execution) - - `src/mcp/tools/simulator/build_run_sim.ts` (xcodebuild, xcrun, plutil, defaults, open) -3. Remove shell operators and use `cwd` instead: - - `src/utils/xcodemake.ts` change `['cd', dir, '&&', 'make']` to `['make']` and pass `{ cwd: dir }`. -4. Optional defensive fallback: if `process.env.PATH` is empty, set it to `/usr/bin:/bin:/usr/sbin:/sbin` at runtime bootstrap. - -## Preventive Measures -- Add a targeted unit/integration test that executes a simple tool with `PATH` cleared and verifies the executor still runs (or fails with a clearer error). -- Avoid defaulting to shell execution for argv-form commands; add lint/test to enforce `useShell` usage only where shell operators are needed. diff --git a/docs/investigations/xcode-ide-state-sync.md b/docs/investigations/xcode-ide-state-sync.md deleted file mode 100644 index 76f66ada..00000000 --- a/docs/investigations/xcode-ide-state-sync.md +++ /dev/null @@ -1,196 +0,0 @@ -# Investigation: Xcode IDE State Sync for Session Defaults - -## Problem Statement - -When XcodeBuildMCP runs inside Xcode's Coding Agent, users expect our tools to use the same scheme and simulator that are currently selected in Xcode's UI toolbar. Currently, users must manually set these via `session-set-defaults` or config files, which creates friction and potential mismatches. - -## Goal - -Auto-detect Xcode's currently selected scheme and simulator (run destination) and sync them to XcodeBuildMCP's session defaults, so tools automatically use the same targets as the IDE. - -## Why This Matters - -1. **Consistency** - When a user builds with Apple's tools in Xcode, then uses our tools to install/launch, they expect the same simulator to be targeted -2. **Reduced friction** - No need to manually configure session defaults when working in Xcode -3. **Avoiding errors** - Prevents "wrong simulator" issues where config says one thing but Xcode shows another - -## Technical Investigation - -### Where Xcode Stores UI State - -Xcode stores the active scheme and run destination in: - -``` -/xcuserdata/.xcuserdatad/UserInterfaceState.xcuserstate -``` - -For xcodeproj (without separate workspace): -``` -.xcodeproj/project.xcworkspace/xcuserdata/.xcuserdatad/UserInterfaceState.xcuserstate -``` - -### File Format: NSKeyedArchiver - -The xcuserstate file is a **binary plist** using Apple's `NSKeyedArchiver` serialization format. This is NOT a simple key-value plist - it's a serialized object graph with: - -- `$objects` array containing all archived objects -- `CF$UID` / `CFKeyedArchiverUID` references between objects -- Nested NSDictionary structures with `NS.keys` and `NS.objects` arrays - -Example structure (via `plutil -p`): -``` -{ - "$archiver" => "NSKeyedArchiver" - "$objects" => [ - 0 => "$null" - 1 => { "$class" => , "NS.keys" => [...], "NS.objects" => [...] } - ... - 407 => "ActiveScheme" - 415 => "ActiveRunDestination" - 730 => "IDERunContextRecentsSchemesKey" - 731 => "IDERunContextRecentsLastUsedRunDestinationBySchemeKey" - ... - ] -} -``` - -### Key Fields Identified - -| Key | Description | Location | -|-----|-------------|----------| -| `ActiveScheme` | Currently selected scheme | Index reference in $objects | -| `ActiveRunDestination` | Current run destination | Index reference in $objects | -| `IDERunContextRecentsSchemesKey` | Dictionary of recent schemes with timestamps | Index 730 (varies) | -| `IDERunContextRecentsLastUsedRunDestinationBySchemeKey` | Maps schemes to their last used destinations | Index 731 (varies) | - -### Run Destination Format - -The run destination (simulator/device) is stored in formats like: - -``` -E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443_iphonesimulator_arm64 -dvtdevice-iphonesimulator:E395B9FD-5A4A-4BE5-B61B-E48D1F5AE443 -``` - -The UUID portion is the simulator identifier that can be used with `xcrun simctl`. - -### Parsing Challenges - -1. **NSKeyedArchiver complexity** - Values aren't stored directly; they're referenced by UID indices that must be followed through the object graph - -2. **No direct key lookup** - Can't simply ask "what is ActiveScheme?"; must find the key string, then trace UID references to find the value - -3. **Scheme detection is fragile** - The scheme name appears multiple times in different contexts; heuristics like "most frequent string" don't reliably identify the *active* scheme - -4. **Simulator detection works better** - The UUID pattern `[A-F0-9-]{36}_iphonesimulator_arm64` is distinctive and reliably identifies simulator destinations - -## Current Implementation (Partially Working) - -Location: `src/utils/xcode-state-reader.ts` - -### What Works -- Finding the xcuserstate file in workspace/project -- Extracting simulator UUID from destination pattern -- Looking up simulator name via `xcrun simctl list devices` - -### What's Fragile -- Scheme detection uses heuristic: "find most frequent scheme-like string" -- This fails when other strings appear more frequently -- Doesn't properly decode NSKeyedArchiver references - -## Options for Improvement - -### Option 1: Proper NSKeyedArchiver Decoding - -**Approach**: Write a proper decoder that follows UID references through the object graph. - -**Pros**: -- Reliable, correct parsing -- Would work for any NSKeyedArchiver file - -**Cons**: -- Complex implementation -- Need to handle various NS* class types -- Maintenance burden - -**Implementation**: Could use Python's `plistlib` with custom unarchiver, or implement UID resolution in TypeScript. - -### Option 2: Improved Heuristic with Known Schemes - -**Approach**: -1. Get list of valid scheme names from `*.xcscheme` files or `xcschememanagement.plist` -2. Search for those specific names near `IDERunContextRecentsSchemesKey` in plutil output -3. Use the one that appears in that context - -**Pros**: -- Simpler than full decoder -- More targeted than current "most frequent" approach - -**Cons**: -- Still a heuristic, could break with Xcode updates -- Requires reading multiple files - -### Option 3: AppleScript/Accessibility API - -**Approach**: Query Xcode directly for its current scheme and destination. - -```bash -osascript -e 'tell application "Xcode" to get name of active scheme of active workspace document' -``` - -**Pros**: -- Gets live state directly from Xcode -- Always accurate to what user sees - -**Cons**: -- Requires Xcode to grant automation permissions -- May not work in all contexts (sandboxing, permissions) -- AppleScript API for Xcode is limited/undocumented - -### Option 4: Partial Sync (Simulator Only) - -**Approach**: Only sync the simulator (which works reliably), skip scheme detection. - -**Pros**: -- Simple, reliable -- Simulator is often the more important value (scheme can be discovered via `list_schemes`) - -**Cons**: -- Incomplete solution -- Agent still needs to determine scheme somehow - -### Option 5: Use xcodebuild to Query - -**Approach**: Use `xcodebuild -showBuildSettings` or similar to get current build context. - -**Cons**: -- These commands don't report the *UI-selected* scheme/destination -- They require you to specify the scheme as a parameter - -## Related Files - -- `src/utils/xcode-state-reader.ts` - Current implementation -- `src/mcp/tools/xcode-ide/sync_xcode_defaults.ts` - Sync tool -- `src/server/bootstrap.ts` - Auto-sync at startup -- `manifests/tools/sync_xcode_defaults.yaml` - Tool manifest - -## Test Project Used - -``` -/Volumes/Developer/XcodeBuildMCP/example_projects/iOS/MCPTest.xcodeproj -``` - -Xcuserstate location: -``` -MCPTest.xcodeproj/project.xcworkspace/xcuserdata/cameroncooke.xcuserdatad/UserInterfaceState.xcuserstate -``` - -## Open Questions - -1. When Xcode's Coding Agent launches the MCP server, what is the working directory? Is it reliably the project directory? - -2. Could we use the `discover_projs` tool output to locate the correct xcuserstate? - -3. Is there a simpler file that contains just the active scheme/destination without the full UI state? - -4. Would Apple's Coding Agent team expose this information via environment variables or a dedicated API? diff --git a/example_projects/iOS/.xcodebuildmcp/config.yaml b/example_projects/iOS/.xcodebuildmcp/config.yaml index be3eb3f0..8edf3397 100644 --- a/example_projects/iOS/.xcodebuildmcp/config.yaml +++ b/example_projects/iOS/.xcodebuildmcp/config.yaml @@ -6,4 +6,4 @@ sessionDefaults: simulatorId: B38FE93D-578B-454B-BE9A-C6FA0CE5F096 useLatestOS: true platform: iOS Simulator - bundleId: com.cameroncooke.MCPTest + bundleId: io.sentry.MCPTest diff --git a/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj b/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj index 0ad3a80d..dde4604c 100644 --- a/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj +++ b/example_projects/iOS/MCPTest.xcodeproj/project.pbxproj @@ -342,7 +342,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTest; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; @@ -384,7 +384,7 @@ ); MACOSX_DEPLOYMENT_TARGET = 15.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTest; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; SUPPORTS_MACCATALYST = NO; @@ -411,7 +411,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.test.MCPTestUITests; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.test.MCPTestUITests; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -437,7 +437,7 @@ GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.test.MCPTestUITests; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.test.MCPTestUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx watchos watchsimulator xros xrsimulator"; diff --git a/example_projects/iOS/MCPTest/ContentView.swift b/example_projects/iOS/MCPTest/ContentView.swift index 210a47a9..b083d334 100644 --- a/example_projects/iOS/MCPTest/ContentView.swift +++ b/example_projects/iOS/MCPTest/ContentView.swift @@ -42,7 +42,7 @@ struct ContentView: View { // OS Log Extension extension Logger { static let myApp = Logger( - subsystem: "com.cameroncooke.MCPTest", + subsystem: "io.sentry.MCPTest", category: "default" ) } diff --git a/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift b/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift index b1c30c97..5461ac73 100644 --- a/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift +++ b/example_projects/iOS/MCPTestUITests/MCPTestUITests.swift @@ -1,7 +1,7 @@ import XCTest /// Reproduction tests for TEST_RUNNER_ environment variable passthrough. -/// GitHub Issue: https://github.com/cameroncooke/XcodeBuildMCP/issues/101 +/// GitHub Issue: https://github.com/getsentry/XcodeBuildMCP/issues/101 /// /// Expected behavior: /// - When invoking xcodebuild test with TEST_RUNNER_USE_DEV_MODE=YES, diff --git a/example_projects/iOS_Calculator/.xcodebuildmcp/config.yaml b/example_projects/iOS_Calculator/.xcodebuildmcp/config.yaml index 75e6d10f..bdd7ad1c 100644 --- a/example_projects/iOS_Calculator/.xcodebuildmcp/config.yaml +++ b/example_projects/iOS_Calculator/.xcodebuildmcp/config.yaml @@ -13,6 +13,6 @@ sessionDefaults: suppressWarnings: false derivedDataPath: ./iOS_Calculator/.derivedData preferXcodebuild: true - bundleId: com.example.calculatorapp + bundleId: io.sentry.calculatorapp simulatorId: B38FE93D-578B-454B-BE9A-C6FA0CE5F096 simulatorPlatform: iOS Simulator diff --git a/example_projects/iOS_Calculator/CalculatorApp.xcodeproj/project.pbxproj b/example_projects/iOS_Calculator/CalculatorApp.xcodeproj/project.pbxproj index 029a697a..097479cb 100644 --- a/example_projects/iOS_Calculator/CalculatorApp.xcodeproj/project.pbxproj +++ b/example_projects/iOS_Calculator/CalculatorApp.xcodeproj/project.pbxproj @@ -414,7 +414,7 @@ DEVELOPMENT_TEAM = BR6WD3M6ZD; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mycompany.CalculatorAppTests; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.CalculatorAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = NO; @@ -434,7 +434,7 @@ DEVELOPMENT_TEAM = BR6WD3M6ZD; GENERATE_INFOPLIST_FILE = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.mycompany.CalculatorAppTests; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.CalculatorAppTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/example_projects/iOS_Calculator/Config/Shared.xcconfig b/example_projects/iOS_Calculator/Config/Shared.xcconfig index 2f68ec6a..f0435598 100644 --- a/example_projects/iOS_Calculator/Config/Shared.xcconfig +++ b/example_projects/iOS_Calculator/Config/Shared.xcconfig @@ -8,7 +8,7 @@ // ========================================== PRODUCT_NAME = CalculatorApp PRODUCT_DISPLAY_NAME = Calculator -PRODUCT_BUNDLE_IDENTIFIER = com.example.calculatorapp +PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp MARKETING_VERSION = 1.0 CURRENT_PROJECT_VERSION = 1 diff --git a/example_projects/iOS_Calculator/Config/Tests.xcconfig b/example_projects/iOS_Calculator/Config/Tests.xcconfig index 974d79ec..572bd47e 100644 --- a/example_projects/iOS_Calculator/Config/Tests.xcconfig +++ b/example_projects/iOS_Calculator/Config/Tests.xcconfig @@ -7,6 +7,6 @@ // ========================================== // Test Target Settings (Customizable by scaffold tool) // ========================================== -PRODUCT_BUNDLE_IDENTIFIER = com.example.calculatorapp +PRODUCT_BUNDLE_IDENTIFIER = io.sentry.calculatorapp TEST_TARGET_NAME = CalculatorApp DEFINES_MODULE = NO diff --git a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj index 48863fab..2efd7d0b 100644 --- a/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj +++ b/example_projects/macOS/MCPTest.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTest; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -365,7 +365,7 @@ "@executable_path/../Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.MCPTest; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; @@ -382,7 +382,7 @@ GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.test.MCPTestTests; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.test.MCPTestTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; @@ -403,7 +403,7 @@ GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 26.0; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.cameroncooke.test.MCPTestTests; + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.test.MCPTestTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; SWIFT_APPROACHABLE_CONCURRENCY = YES; diff --git a/package.json b/package.json index 2f83dcab..c2763921 100644 --- a/package.json +++ b/package.json @@ -58,16 +58,15 @@ "macos", "simulator" ], - "author": "Cameron Cooke", "license": "MIT", - "description": "XcodeBuildMCP is a ModelContextProtocol server that provides tools for Xcode project management, simulator management, and app utilities.", + "description": "XcodeBuildMCP is a Model Context Protocol server that provides tools for Xcode project management, simulator management, and app utilities.", "repository": { "type": "git", - "url": "git+https://github.com/cameroncooke/XcodeBuildMCP.git" + "url": "git+https://github.com/getsentry/XcodeBuildMCP.git" }, - "homepage": "https://www.async-let.com/blog/xcodebuild-mcp/", + "homepage": "https://www.xcodebuildmcp.com", "bugs": { - "url": "https://github.com/cameroncooke/XcodeBuildMCP/issues" + "url": "https://github.com/getsentry/XcodeBuildMCP/issues" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.25.1", @@ -104,4 +103,4 @@ "vitest": "^3.2.4", "xcode": "^3.0.1" } -} +} \ No newline at end of file diff --git a/scripts/create-homebrew-formula.sh b/scripts/create-homebrew-formula.sh index 5d7a007d..1b4a6706 100755 --- a/scripts/create-homebrew-formula.sh +++ b/scripts/create-homebrew-formula.sh @@ -70,13 +70,13 @@ if [[ -z "$VERSION" || -z "$ARM64_SHA" || -z "$X64_SHA" ]]; then fi if [[ -z "$BASE_URL" ]]; then - BASE_URL="https://github.com/cameroncooke/XcodeBuildMCP/releases/download/v$VERSION" + BASE_URL="https://github.com/getsentry/XcodeBuildMCP/releases/download/v$VERSION" fi FORMULA_CONTENT="$(cat < { it('should validate schema with valid inputs', () => { const schemaObj = z.strictObject(schema); expect(schemaObj.safeParse({}).success).toBe(true); - expect(schemaObj.safeParse({ bundleId: 'com.example.app' }).success).toBe(false); + expect(schemaObj.safeParse({ bundleId: 'io.sentry.app' }).success).toBe(false); expect(Object.keys(schema).sort()).toEqual(['env']); }); @@ -73,7 +73,7 @@ describe('launch_app_device plugin (device-shared)', () => { await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, trackingExecutor, createMockFileSystemExecutor(), @@ -91,7 +91,7 @@ describe('launch_app_device plugin (device-shared)', () => { '--json-output', expect.stringMatching(/^\/.*\/launch-\d+\.json$/), '--terminate-existing', - 'com.example.app', + 'io.sentry.app', ]); expect(calls[0].logPrefix).toBe('Launch app on device'); expect(calls[0].useShell).toBe(false); @@ -151,7 +151,7 @@ describe('launch_app_device plugin (device-shared)', () => { await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', env: { STAGING_ENABLED: '1', DEBUG: 'true' }, }, trackingExecutor, @@ -161,7 +161,7 @@ describe('launch_app_device plugin (device-shared)', () => { expect(calls).toHaveLength(1); const cmd = calls[0].command; // bundleId should be the last element - expect(cmd[cmd.length - 1]).toBe('com.example.app'); + expect(cmd[cmd.length - 1]).toBe('io.sentry.app'); // --environment-variables should be provided exactly once as JSON const envFlagIndices = cmd .map((part: string, index: number) => (part === '--environment-variables' ? index : -1)) @@ -187,7 +187,7 @@ describe('launch_app_device plugin (device-shared)', () => { await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, trackingExecutor, createMockFileSystemExecutor(), @@ -207,7 +207,7 @@ describe('launch_app_device plugin (device-shared)', () => { const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, createMockFileSystemExecutor(), @@ -233,7 +233,7 @@ describe('launch_app_device plugin (device-shared)', () => { const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, createMockFileSystemExecutor(), @@ -271,7 +271,7 @@ describe('launch_app_device plugin (device-shared)', () => { const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, mockFileSystem, @@ -298,13 +298,13 @@ describe('launch_app_device plugin (device-shared)', () => { it('should handle successful launch with command output', async () => { const mockExecutor = createMockExecutor({ success: true, - output: 'App "com.example.app" launched on device "test-device-123"', + output: 'App "io.sentry.app" launched on device "test-device-123"', }); const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, createMockFileSystemExecutor(), @@ -314,7 +314,7 @@ describe('launch_app_device plugin (device-shared)', () => { content: [ { type: 'text', - text: '✅ App launched successfully\n\nApp "com.example.app" launched on device "test-device-123"', + text: '✅ App launched successfully\n\nApp "io.sentry.app" launched on device "test-device-123"', }, ], nextSteps: [], @@ -358,7 +358,7 @@ describe('launch_app_device plugin (device-shared)', () => { const result = await launch_app_deviceLogic( { deviceId: 'test-device-invalid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, createMockFileSystemExecutor(), @@ -381,7 +381,7 @@ describe('launch_app_device plugin (device-shared)', () => { const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, createMockFileSystemExecutor(), @@ -404,7 +404,7 @@ describe('launch_app_device plugin (device-shared)', () => { const result = await launch_app_deviceLogic( { deviceId: 'test-device-123', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }, mockExecutor, createMockFileSystemExecutor(), diff --git a/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts b/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts index 4be522b0..836e7eb9 100644 --- a/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/start_device_log_cap.test.ts @@ -105,7 +105,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, @@ -135,7 +135,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, @@ -365,7 +365,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, @@ -396,7 +396,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, @@ -434,7 +434,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, @@ -467,7 +467,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, @@ -500,7 +500,7 @@ describe('start_device_log_cap plugin', () => { const result = await start_device_log_capLogic( { deviceId: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, mockExecutor, mockFileSystemExecutor, diff --git a/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts b/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts index bf42b977..b1648446 100644 --- a/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/start_sim_log_cap.test.ts @@ -72,7 +72,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: 'app', }, mockExecutor, @@ -97,7 +97,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: 'app', }, mockExecutor, @@ -132,7 +132,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: 'swiftui', }, mockExecutor, @@ -158,7 +158,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: 'all', }, mockExecutor, @@ -183,7 +183,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: ['com.apple.UIKit', 'com.apple.CoreData'], }, mockExecutor, @@ -209,7 +209,7 @@ describe('start_sim_log_cap plugin', () => { const result = await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', captureConsole: true, subsystemFilter: 'app', }, @@ -269,7 +269,7 @@ describe('start_sim_log_cap plugin', () => { await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', captureConsole: true, subsystemFilter: 'app', }, @@ -287,7 +287,7 @@ describe('start_sim_log_cap plugin', () => { '--console-pty', '--terminate-running-process', 'test-uuid', - 'com.example.app', + 'io.sentry.app', ], }); expect(spawnCalls[1]).toEqual({ @@ -300,7 +300,7 @@ describe('start_sim_log_cap plugin', () => { 'stream', '--level=debug', '--predicate', - 'subsystem == "com.example.app"', + 'subsystem == "io.sentry.app"', ], }); }); @@ -339,7 +339,7 @@ describe('start_sim_log_cap plugin', () => { await start_sim_log_capLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', captureConsole: false, subsystemFilter: 'app', }, @@ -359,7 +359,7 @@ describe('start_sim_log_cap plugin', () => { 'stream', '--level=debug', '--predicate', - 'subsystem == "com.example.app"', + 'subsystem == "io.sentry.app"', ], }); }); diff --git a/src/mcp/tools/logging/__tests__/stop_device_log_cap.test.ts b/src/mcp/tools/logging/__tests__/stop_device_log_cap.test.ts index df805eda..2ec1888f 100644 --- a/src/mcp/tools/logging/__tests__/stop_device_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/stop_device_log_cap.test.ts @@ -75,13 +75,13 @@ describe('stop_device_log_cap plugin', () => { const result = await stop_device_log_capLogic( { - logSessionId: 'device-log-00008110-001A2C3D4E5F-com.example.MyApp', + logSessionId: 'device-log-00008110-001A2C3D4E5F-io.sentry.MyApp', }, mockFileSystem, ); expect(result.content[0].text).toBe( - 'Failed to stop device log capture session device-log-00008110-001A2C3D4E5F-com.example.MyApp: Device log capture session not found: device-log-00008110-001A2C3D4E5F-com.example.MyApp', + 'Failed to stop device log capture session device-log-00008110-001A2C3D4E5F-io.sentry.MyApp: Device log capture session not found: device-log-00008110-001A2C3D4E5F-io.sentry.MyApp', ); expect(result.isError).toBe(true); }); @@ -101,7 +101,7 @@ describe('stop_device_log_cap plugin', () => { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', hasEnded: false, }); @@ -146,7 +146,7 @@ describe('stop_device_log_cap plugin', () => { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', hasEnded: false, }); @@ -188,7 +188,7 @@ describe('stop_device_log_cap plugin', () => { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', hasEnded: false, }); @@ -230,7 +230,7 @@ describe('stop_device_log_cap plugin', () => { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', hasEnded: false, }); @@ -274,7 +274,7 @@ describe('stop_device_log_cap plugin', () => { process: testProcess as unknown as DeviceLogSession['process'], logFilePath: testLogFilePath, deviceUuid: '00008110-001A2C3D4E5F', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', hasEnded: false, }); diff --git a/src/mcp/tools/project-discovery/__tests__/get_app_bundle_id.test.ts b/src/mcp/tools/project-discovery/__tests__/get_app_bundle_id.test.ts index cde2704d..f80df8bf 100644 --- a/src/mcp/tools/project-discovery/__tests__/get_app_bundle_id.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/get_app_bundle_id.test.ts @@ -92,7 +92,7 @@ describe('get_app_bundle_id plugin', () => { it('should return success with bundle ID using defaults read', async () => { const mockExecutor = createMockExecutorForCommands({ - 'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': 'com.example.MyApp', + 'defaults read "/path/to/MyApp.app/Info" CFBundleIdentifier': 'io.sentry.MyApp', }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, @@ -108,7 +108,7 @@ describe('get_app_bundle_id plugin', () => { content: [ { type: 'text', - text: '✅ Bundle ID: com.example.MyApp', + text: '✅ Bundle ID: io.sentry.MyApp', }, ], nextSteps: [ @@ -121,7 +121,7 @@ describe('get_app_bundle_id plugin', () => { { tool: 'launch_app_sim', label: 'Launch on simulator', - params: { simulatorId: 'SIMULATOR_UUID', bundleId: 'com.example.MyApp' }, + params: { simulatorId: 'SIMULATOR_UUID', bundleId: 'io.sentry.MyApp' }, priority: 2, }, { @@ -133,7 +133,7 @@ describe('get_app_bundle_id plugin', () => { { tool: 'launch_app_device', label: 'Launch on device', - params: { deviceId: 'DEVICE_UDID', bundleId: 'com.example.MyApp' }, + params: { deviceId: 'DEVICE_UDID', bundleId: 'io.sentry.MyApp' }, priority: 4, }, ], @@ -147,7 +147,7 @@ describe('get_app_bundle_id plugin', () => { 'defaults read failed', ), '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/path/to/MyApp.app/Info.plist"': - 'com.example.MyApp', + 'io.sentry.MyApp', }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, @@ -163,7 +163,7 @@ describe('get_app_bundle_id plugin', () => { content: [ { type: 'text', - text: '✅ Bundle ID: com.example.MyApp', + text: '✅ Bundle ID: io.sentry.MyApp', }, ], nextSteps: [ @@ -176,7 +176,7 @@ describe('get_app_bundle_id plugin', () => { { tool: 'launch_app_sim', label: 'Launch on simulator', - params: { simulatorId: 'SIMULATOR_UUID', bundleId: 'com.example.MyApp' }, + params: { simulatorId: 'SIMULATOR_UUID', bundleId: 'io.sentry.MyApp' }, priority: 2, }, { @@ -188,7 +188,7 @@ describe('get_app_bundle_id plugin', () => { { tool: 'launch_app_device', label: 'Launch on device', - params: { deviceId: 'DEVICE_UDID', bundleId: 'com.example.MyApp' }, + params: { deviceId: 'DEVICE_UDID', bundleId: 'io.sentry.MyApp' }, priority: 4, }, ], diff --git a/src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts b/src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts index a578fbca..7c33b332 100644 --- a/src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/get_mac_bundle_id.test.ts @@ -71,7 +71,7 @@ describe('get_mac_bundle_id plugin', () => { it('should return success with bundle ID using defaults read', async () => { const mockExecutor = createMockExecutorForCommands({ 'defaults read "/Applications/MyApp.app/Contents/Info" CFBundleIdentifier': - 'com.example.MyMacApp', + 'io.sentry.MyMacApp', }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, @@ -87,7 +87,7 @@ describe('get_mac_bundle_id plugin', () => { content: [ { type: 'text', - text: '✅ Bundle ID: com.example.MyMacApp', + text: '✅ Bundle ID: io.sentry.MyMacApp', }, ], nextSteps: [ @@ -114,7 +114,7 @@ describe('get_mac_bundle_id plugin', () => { 'defaults read failed', ), '/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" "/Applications/MyApp.app/Contents/Info.plist"': - 'com.example.MyMacApp', + 'io.sentry.MyMacApp', }); const mockFileSystemExecutor = createMockFileSystemExecutor({ existsSync: () => true, @@ -130,7 +130,7 @@ describe('get_mac_bundle_id plugin', () => { content: [ { type: 'text', - text: '✅ Bundle ID: com.example.MyMacApp', + text: '✅ Bundle ID: io.sentry.MyMacApp', }, ], nextSteps: [ diff --git a/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts b/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts index e96ef473..65e9bac4 100644 --- a/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/show_build_settings.test.ts @@ -60,7 +60,7 @@ describe('show_build_settings plugin', () => { BUILD_DIR = /Users/dev/Build/Products CONFIGURATION = Debug DEVELOPMENT_TEAM = ABC123DEF4 - PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MyApp PRODUCT_NAME = MyApp SUPPORTED_PLATFORMS = iphoneos iphonesimulator`, error: undefined, @@ -108,7 +108,7 @@ describe('show_build_settings plugin', () => { BUILD_DIR = /Users/dev/Build/Products CONFIGURATION = Debug DEVELOPMENT_TEAM = ABC123DEF4 - PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MyApp PRODUCT_NAME = MyApp SUPPORTED_PLATFORMS = iphoneos iphonesimulator`, }, @@ -268,7 +268,7 @@ describe('show_build_settings plugin', () => { BUILD_DIR = /Users/dev/Build/Products CONFIGURATION = Debug DEVELOPMENT_TEAM = ABC123DEF4 - PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MyApp PRODUCT_NAME = MyApp SUPPORTED_PLATFORMS = iphoneos iphonesimulator`, error: undefined, @@ -316,7 +316,7 @@ describe('show_build_settings plugin', () => { BUILD_DIR = /Users/dev/Build/Products CONFIGURATION = Debug DEVELOPMENT_TEAM = ABC123DEF4 - PRODUCT_BUNDLE_IDENTIFIER = com.example.MyApp + PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MyApp PRODUCT_NAME = MyApp SUPPORTED_PLATFORMS = iphoneos iphonesimulator`, }, diff --git a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts index 6e759547..d889e11e 100644 --- a/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts +++ b/src/mcp/tools/project-scaffolding/__tests__/scaffold_ios_project.test.ts @@ -177,7 +177,7 @@ describe('scaffold_ios_project plugin', () => { '-o', expect.stringMatching(/template\.zip$/), expect.stringMatching( - /https:\/\/github\.com\/cameroncooke\/XcodeBuildMCP-iOS-Template\/releases\/download\/v\d+\.\d+\.\d+\/XcodeBuildMCP-iOS-Template-\d+\.\d+\.\d+\.zip/, + /https:\/\/github\.com\/getsentry\/XcodeBuildMCP-iOS-Template\/releases\/download\/v\d+\.\d+\.\d+\/XcodeBuildMCP-iOS-Template-\d+\.\d+\.\d+\.zip/, ), ]); @@ -273,7 +273,7 @@ describe('scaffold_ios_project plugin', () => { '-f', '-o', expect.stringMatching(/template\.zip$/), - 'https://github.com/cameroncooke/XcodeBuildMCP-iOS-Template/releases/download/v2.0.0/XcodeBuildMCP-iOS-Template-2.0.0.zip', + 'https://github.com/getsentry/XcodeBuildMCP-iOS-Template/releases/download/v2.0.0/XcodeBuildMCP-iOS-Template-2.0.0.zip', ]); await initConfigStoreForTest({ iosTemplatePath: '/mock/template/path' }); diff --git a/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts b/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts index 9a7d94c6..d3a7ef2e 100644 --- a/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts +++ b/src/mcp/tools/project-scaffolding/__tests__/scaffold_macos_project.test.ts @@ -119,7 +119,7 @@ describe('scaffold_macos_project plugin', () => { // This test validates that the curl command would be generated correctly // by verifying the URL construction logic const expectedUrl = - 'https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/'; + 'https://github.com/getsentry/XcodeBuildMCP-macOS-Template/releases/download/'; // The curl command should be structured correctly for macOS template expect(expectedUrl).toContain('XcodeBuildMCP-macOS-Template'); @@ -156,7 +156,7 @@ describe('scaffold_macos_project plugin', () => { it('should generate correct commands for template with version', async () => { // This test validates that the curl command would be generated correctly with version const testVersion = 'v1.0.0'; - const expectedUrlWithVersion = `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`; + const expectedUrlWithVersion = `https://github.com/getsentry/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`; // The URL should contain the specific version expect(expectedUrlWithVersion).toContain(testVersion); @@ -168,7 +168,7 @@ describe('scaffold_macos_project plugin', () => { // The full URL should be correctly constructed expect(expectedUrlWithVersion).toBe( - `https://github.com/cameroncooke/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`, + `https://github.com/getsentry/XcodeBuildMCP-macOS-Template/releases/download/${testVersion}/`, ); }); diff --git a/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts b/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts index eefd9f6c..9e4d5ff1 100644 --- a/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts +++ b/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts @@ -129,7 +129,7 @@ function updateXCConfigFile(content: string, params: Record): s ); result = result.replace( /PRODUCT_BUNDLE_IDENTIFIER = .+/g, - `PRODUCT_BUNDLE_IDENTIFIER = ${bundleIdentifier ?? `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`, + `PRODUCT_BUNDLE_IDENTIFIER = ${bundleIdentifier ?? `io.sentry.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`, ); result = result.replace( /MARKETING_VERSION = .+/g, @@ -290,8 +290,7 @@ async function processFile( } else { // Use standard placeholder replacement const bundleIdentifier = - bundleIdentifierParam ?? - `com.example.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`; + bundleIdentifierParam ?? `io.sentry.${projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`; processedContent = replacePlaceholders(content, projectName, bundleIdentifier); } diff --git a/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts b/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts index 2e9cced8..f10911b1 100644 --- a/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts +++ b/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts @@ -80,7 +80,7 @@ function updateXCConfigFile( ); result = result.replace( /PRODUCT_BUNDLE_IDENTIFIER = .+/g, - `PRODUCT_BUNDLE_IDENTIFIER = ${params.bundleIdentifier ?? `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`, + `PRODUCT_BUNDLE_IDENTIFIER = ${params.bundleIdentifier ?? `io.sentry.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`}`, ); result = result.replace( /MARKETING_VERSION = .+/g, @@ -204,7 +204,7 @@ async function processFile( // Use standard placeholder replacement const bundleIdentifier = params.bundleIdentifier ?? - `com.example.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`; + `io.sentry.${params.projectName.toLowerCase().replace(/[^a-z0-9]/g, '')}`; processedContent = replacePlaceholders(content, params.projectName, bundleIdentifier); } diff --git a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts index 4772a13c..98901872 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -168,7 +168,7 @@ describe('build_run_sim tool', () => { // Bundle ID extraction return createMockCommandResponse({ success: true, - output: 'com.example.MyApp', + output: 'io.sentry.MyApp', }); } else { // All other commands (boot, open, install, launch) succeed diff --git a/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts index 20d858dc..9be4dc06 100644 --- a/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts @@ -251,7 +251,7 @@ describe('install_app_sim tool', () => { return Promise.resolve( createMockCommandResponse({ success: true, - output: 'com.example.myapp', + output: 'io.sentry.myapp', error: undefined, }), ); @@ -287,7 +287,7 @@ describe('install_app_sim tool', () => { { tool: 'launch_app_sim', label: 'Launch the app', - params: { simulatorId: 'test-uuid-123', bundleId: 'com.example.myapp' }, + params: { simulatorId: 'test-uuid-123', bundleId: 'io.sentry.myapp' }, priority: 2, }, ], diff --git a/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts b/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts index 1c2e4b29..11e0800d 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_logs_sim.test.ts @@ -25,7 +25,7 @@ describe('launch_app_logs_sim tool', () => { expect(schemaObj.safeParse({}).success).toBe(true); expect(schemaObj.safeParse({ args: ['--debug'] }).success).toBe(true); - expect(schemaObj.safeParse({ bundleId: 'com.example.app' }).success).toBe(false); + expect(schemaObj.safeParse({ bundleId: 'io.sentry.app' }).success).toBe(false); expect(schemaObj.safeParse({ bundleId: 42 }).success).toBe(false); expect(Object.keys(schema).sort()).toEqual(['args', 'env']); @@ -76,7 +76,7 @@ describe('launch_app_logs_sim tool', () => { const result = await launch_app_logs_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, mockExecutor, logCaptureStub, @@ -102,7 +102,7 @@ describe('launch_app_logs_sim tool', () => { expect(capturedParams).toEqual({ simulatorUuid: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', captureConsole: true, }); }); @@ -124,7 +124,7 @@ describe('launch_app_logs_sim tool', () => { await launch_app_logs_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', args: ['--debug'], }, mockExecutor, @@ -133,7 +133,7 @@ describe('launch_app_logs_sim tool', () => { expect(capturedParams).toEqual({ simulatorUuid: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', captureConsole: true, args: ['--debug'], }); @@ -156,7 +156,7 @@ describe('launch_app_logs_sim tool', () => { await launch_app_logs_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', env: { STAGING_ENABLED: '1' }, }, mockExecutor, @@ -165,7 +165,7 @@ describe('launch_app_logs_sim tool', () => { expect(capturedParams).toEqual({ simulatorUuid: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', captureConsole: true, env: { STAGING_ENABLED: '1' }, }); @@ -188,7 +188,7 @@ describe('launch_app_logs_sim tool', () => { await launch_app_logs_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, mockExecutor, logCaptureStub, @@ -196,7 +196,7 @@ describe('launch_app_logs_sim tool', () => { expect(capturedParams).toEqual({ simulatorUuid: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', captureConsole: true, }); }); @@ -214,7 +214,7 @@ describe('launch_app_logs_sim tool', () => { const result = await launch_app_logs_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, mockExecutor, logCaptureStub, diff --git a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts index 957235b6..abb102dc 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts @@ -21,7 +21,7 @@ describe('launch_app_sim tool', () => { }).success, ).toBe(true); - expect(schemaObj.safeParse({ bundleId: 'com.example.testapp' }).success).toBe(false); + expect(schemaObj.safeParse({ bundleId: 'io.sentry.testapp' }).success).toBe(false); expect(schemaObj.safeParse({ bundleId: 123 }).success).toBe(false); expect(Object.keys(schema).sort()).toEqual(['args', 'env']); @@ -36,7 +36,7 @@ describe('launch_app_sim tool', () => { describe('Handler Requirements', () => { it('should require simulator identifier when not provided', async () => { - const result = await handler({ bundleId: 'com.example.testapp' }); + const result = await handler({ bundleId: 'io.sentry.testapp' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); @@ -58,7 +58,7 @@ describe('launch_app_sim tool', () => { const result = await handler({ simulatorId: 'SIM-UUID', simulatorName: 'iPhone 16', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }); expect(result.isError).toBe(true); @@ -92,7 +92,7 @@ describe('launch_app_sim tool', () => { const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, sequencedExecutor, ); @@ -114,7 +114,7 @@ describe('launch_app_sim tool', () => { { tool: 'start_sim_log_cap', label: 'Capture structured logs (app continues running)', - params: { simulatorId: 'test-uuid-123', bundleId: 'com.example.testapp' }, + params: { simulatorId: 'test-uuid-123', bundleId: 'io.sentry.testapp' }, priority: 2, }, { @@ -122,7 +122,7 @@ describe('launch_app_sim tool', () => { label: 'Capture console + structured logs (app restarts)', params: { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', captureConsole: true, }, priority: 3, @@ -157,23 +157,15 @@ describe('launch_app_sim tool', () => { await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', args: ['--debug', '--verbose'], }, sequencedExecutor, ); expect(commands).toEqual([ - ['xcrun', 'simctl', 'get_app_container', 'test-uuid-123', 'com.example.testapp', 'app'], - [ - 'xcrun', - 'simctl', - 'launch', - 'test-uuid-123', - 'com.example.testapp', - '--debug', - '--verbose', - ], + ['xcrun', 'simctl', 'get_app_container', 'test-uuid-123', 'io.sentry.testapp', 'app'], + ['xcrun', 'simctl', 'launch', 'test-uuid-123', 'io.sentry.testapp', '--debug', '--verbose'], ]); }); @@ -201,7 +193,7 @@ describe('launch_app_sim tool', () => { { simulatorId: 'resolved-uuid', simulatorName: 'iPhone 16', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, sequencedExecutor, ); @@ -223,7 +215,7 @@ describe('launch_app_sim tool', () => { { tool: 'start_sim_log_cap', label: 'Capture structured logs (app continues running)', - params: { simulatorId: 'resolved-uuid', bundleId: 'com.example.testapp' }, + params: { simulatorId: 'resolved-uuid', bundleId: 'io.sentry.testapp' }, priority: 2, }, { @@ -231,7 +223,7 @@ describe('launch_app_sim tool', () => { label: 'Capture console + structured logs (app restarts)', params: { simulatorId: 'resolved-uuid', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', captureConsole: true, }, priority: 3, @@ -261,7 +253,7 @@ describe('launch_app_sim tool', () => { const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, mockExecutor, ); @@ -293,7 +285,7 @@ describe('launch_app_sim tool', () => { const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, mockExecutor, ); @@ -332,7 +324,7 @@ describe('launch_app_sim tool', () => { const result = await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, mockExecutor, ); @@ -378,7 +370,7 @@ describe('launch_app_sim tool', () => { await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', env: { STAGING_ENABLED: '1', DEBUG: 'true' }, }, sequencedExecutor, @@ -424,7 +416,7 @@ describe('launch_app_sim tool', () => { await launch_app_simLogic( { simulatorId: 'test-uuid-123', - bundleId: 'com.example.testapp', + bundleId: 'io.sentry.testapp', }, sequencedExecutor, ); diff --git a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts index 19d68415..2ad91e7b 100644 --- a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts @@ -18,7 +18,7 @@ describe('stop_app_sim tool', () => { const schemaObj = z.object(schema); expect(schemaObj.safeParse({}).success).toBe(true); - expect(schemaObj.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); + expect(schemaObj.safeParse({ bundleId: 'io.sentry.app' }).success).toBe(true); expect(schemaObj.safeParse({ bundleId: 42 }).success).toBe(true); expect(Object.keys(schema)).toEqual([]); @@ -35,7 +35,7 @@ describe('stop_app_sim tool', () => { describe('Handler Requirements', () => { it('should require simulator identifier when not provided', async () => { - const result = await handler({ bundleId: 'com.example.app' }); + const result = await handler({ bundleId: 'io.sentry.app' }); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); @@ -57,7 +57,7 @@ describe('stop_app_sim tool', () => { const result = await handler({ simulatorId: 'SIM-UUID', simulatorName: 'iPhone 16', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', }); expect(result.isError).toBe(true); @@ -74,7 +74,7 @@ describe('stop_app_sim tool', () => { const result = await stop_app_simLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.App', + bundleId: 'io.sentry.App', }, mockExecutor, ); @@ -83,7 +83,7 @@ describe('stop_app_sim tool', () => { content: [ { type: 'text', - text: 'App com.example.App stopped successfully in simulator test-uuid', + text: 'App io.sentry.App stopped successfully in simulator test-uuid', }, ], }); @@ -96,7 +96,7 @@ describe('stop_app_sim tool', () => { { simulatorId: 'resolved-uuid', simulatorName: 'iPhone 16', - bundleId: 'com.example.App', + bundleId: 'io.sentry.App', }, mockExecutor, ); @@ -105,7 +105,7 @@ describe('stop_app_sim tool', () => { content: [ { type: 'text', - text: 'App com.example.App stopped successfully in simulator "iPhone 16" (resolved-uuid)', + text: 'App io.sentry.App stopped successfully in simulator "iPhone 16" (resolved-uuid)', }, ], }); @@ -121,7 +121,7 @@ describe('stop_app_sim tool', () => { const result = await stop_app_simLogic( { simulatorId: 'invalid-uuid', - bundleId: 'com.example.App', + bundleId: 'io.sentry.App', }, terminateExecutor, ); @@ -145,7 +145,7 @@ describe('stop_app_sim tool', () => { const result = await stop_app_simLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.App', + bundleId: 'io.sentry.App', }, throwingExecutor, ); @@ -188,14 +188,14 @@ describe('stop_app_sim tool', () => { await stop_app_simLogic( { simulatorId: 'test-uuid', - bundleId: 'com.example.App', + bundleId: 'io.sentry.App', }, trackingExecutor, ); expect(calls).toEqual([ { - command: ['xcrun', 'simctl', 'terminate', 'test-uuid', 'com.example.App'], + command: ['xcrun', 'simctl', 'terminate', 'test-uuid', 'io.sentry.App'], logPrefix: 'Stop App in Simulator', useShell: false, opts: undefined, diff --git a/src/mcp/tools/ui-automation/__tests__/button.test.ts b/src/mcp/tools/ui-automation/__tests__/button.test.ts index 08305d4a..c64d426b 100644 --- a/src/mcp/tools/ui-automation/__tests__/button.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/button.test.ts @@ -11,6 +11,7 @@ import { } from '../../../../test-utils/mock-executors.ts'; import { schema, handler, buttonLogic } from '../button.ts'; import type { CommandExecutor } from '../../../../utils/execution/index.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; describe('Button Plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -323,7 +324,7 @@ describe('Button Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -343,7 +344,7 @@ describe('Button Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/gesture.test.ts b/src/mcp/tools/ui-automation/__tests__/gesture.test.ts index 7cfaccd6..d05e8600 100644 --- a/src/mcp/tools/ui-automation/__tests__/gesture.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/gesture.test.ts @@ -11,6 +11,7 @@ import { } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, gestureLogic } from '../gesture.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; describe('Gesture Plugin', () => { beforeEach(() => { @@ -342,7 +343,7 @@ describe('Gesture Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -362,7 +363,7 @@ describe('Gesture Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/key_press.test.ts b/src/mcp/tools/ui-automation/__tests__/key_press.test.ts index 8d2c551a..bb0e8275 100644 --- a/src/mcp/tools/ui-automation/__tests__/key_press.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/key_press.test.ts @@ -12,6 +12,7 @@ import { } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, key_pressLogic } from '../key_press.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; function createDefaultMockAxeHelpers() { return { @@ -21,7 +22,7 @@ function createDefaultMockAxeHelpers() { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\\n\\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -201,7 +202,7 @@ describe('Key Press Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -288,7 +289,7 @@ describe('Key Press Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -308,7 +309,7 @@ describe('Key Press Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts index 20e6798b..47443638 100644 --- a/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts @@ -11,6 +11,7 @@ import { } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, key_sequenceLogic } from '../key_sequence.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; describe('Key Sequence Tool', () => { beforeEach(() => { @@ -86,7 +87,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -131,7 +132,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -179,7 +180,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -224,7 +225,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -277,7 +278,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -316,7 +317,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -346,7 +347,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -366,7 +367,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -387,7 +388,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -426,7 +427,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -460,7 +461,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -494,7 +495,7 @@ describe('Key Sequence Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/long_press.test.ts b/src/mcp/tools/ui-automation/__tests__/long_press.test.ts index a1c2fb25..3cbe9dc4 100644 --- a/src/mcp/tools/ui-automation/__tests__/long_press.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/long_press.test.ts @@ -7,6 +7,7 @@ import * as z from 'zod'; import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, long_pressLogic } from '../long_press.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; describe('Long Press Plugin', () => { beforeEach(() => { @@ -343,7 +344,7 @@ describe('Long Press Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -365,7 +366,7 @@ describe('Long Press Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts b/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts index f8dca02e..bb73d1c5 100644 --- a/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts @@ -7,6 +7,7 @@ import * as z from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { schema, handler, snapshot_uiLogic } from '../snapshot_ui.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; describe('Snapshot UI Plugin', () => { describe('Export Field Validation (Literal)', () => { @@ -133,7 +134,7 @@ describe('Snapshot UI Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -152,7 +153,7 @@ describe('Snapshot UI Plugin', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/swipe.test.ts b/src/mcp/tools/ui-automation/__tests__/swipe.test.ts index 5a388947..165327b6 100644 --- a/src/mcp/tools/ui-automation/__tests__/swipe.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/swipe.test.ts @@ -9,6 +9,7 @@ import { SystemError } from '../../../../utils/responses/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, type AxeHelpers, swipeLogic, type SwipeParams } from '../swipe.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; // Helper function to create mock axe helpers function createMockAxeHelpers(): AxeHelpers { @@ -19,7 +20,7 @@ function createMockAxeHelpers(): AxeHelpers { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -36,7 +37,7 @@ function createMockAxeHelpersWithNullPath(): AxeHelpers { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -423,7 +424,7 @@ describe('Swipe Tool', () => { content: [ { type: 'text' as const, - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/tap.test.ts b/src/mcp/tools/ui-automation/__tests__/tap.test.ts index 24c6c714..9f19de5a 100644 --- a/src/mcp/tools/ui-automation/__tests__/tap.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/tap.test.ts @@ -8,6 +8,7 @@ import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, type AxeHelpers, tapLogic } from '../tap.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; // Helper function to create mock axe helpers function createMockAxeHelpers(): AxeHelpers { @@ -18,7 +19,7 @@ function createMockAxeHelpers(): AxeHelpers { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -35,7 +36,7 @@ function createMockAxeHelpersWithNullPath(): AxeHelpers { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -803,7 +804,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -833,7 +834,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -863,7 +864,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -891,7 +892,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -919,7 +920,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -947,7 +948,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/touch.test.ts b/src/mcp/tools/ui-automation/__tests__/touch.test.ts index fa955e37..3f83d031 100644 --- a/src/mcp/tools/ui-automation/__tests__/touch.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/touch.test.ts @@ -8,6 +8,7 @@ import * as z from 'zod'; import { createMockExecutor, mockProcess } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, touchLogic } from '../touch.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; describe('Touch Plugin', () => { beforeEach(() => { @@ -124,7 +125,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -174,7 +175,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -224,7 +225,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -276,7 +277,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -367,7 +368,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -389,7 +390,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -405,7 +406,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -443,7 +444,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -504,7 +505,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -547,7 +548,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -590,7 +591,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -630,7 +631,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -652,7 +653,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -673,7 +674,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -714,7 +715,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -757,7 +758,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -800,7 +801,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/ui-automation/__tests__/type_text.test.ts b/src/mcp/tools/ui-automation/__tests__/type_text.test.ts index 10940956..3910667a 100644 --- a/src/mcp/tools/ui-automation/__tests__/type_text.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/type_text.test.ts @@ -11,6 +11,7 @@ import { } from '../../../../test-utils/mock-executors.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; import { schema, handler, type_textLogic } from '../type_text.ts'; +import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts'; // Mock axe helpers for dependency injection function createMockAxeHelpers( @@ -27,7 +28,7 @@ function createMockAxeHelpers( content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -306,7 +307,7 @@ describe('Type Text Tool', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, @@ -384,7 +385,7 @@ describe('Type Text Tool', () => { content: [ { type: 'text', - text: 'AXe tool not found. UI automation features are not available.\n\nInstall AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\nEnsure bundled artifacts are included or PATH is configured.', + text: AXE_NOT_AVAILABLE_MESSAGE, }, ], isError: true, diff --git a/src/mcp/tools/xcode-ide/__tests__/sync_xcode_defaults.test.ts b/src/mcp/tools/xcode-ide/__tests__/sync_xcode_defaults.test.ts index 6d8d1e20..5a1b9ff8 100644 --- a/src/mcp/tools/xcode-ide/__tests__/sync_xcode_defaults.test.ts +++ b/src/mcp/tools/xcode-ide/__tests__/sync_xcode_defaults.test.ts @@ -9,7 +9,7 @@ import { schema, syncXcodeDefaultsLogic } from '../sync_xcode_defaults.ts'; const EXAMPLE_PROJECT_PATH = join(process.cwd(), 'example_projects/iOS/MCPTest.xcodeproj'); const EXAMPLE_XCUSERSTATE = join( EXAMPLE_PROJECT_PATH, - 'project.xcworkspace/xcuserdata/cameroncooke.xcuserdatad/UserInterfaceState.xcuserstate', + 'project.xcworkspace/xcuserdata/johndoe.xcuserdatad/UserInterfaceState.xcuserstate', ); describe('sync_xcode_defaults tool', () => { @@ -59,18 +59,18 @@ describe('sync_xcode_defaults tool', () => { async () => { const simctlOutput = JSON.stringify({ devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [ - { udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', name: 'Apple Vision Pro' }, + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0': [ + { udid: 'B38FE93D-578B-454B-BE9A-C6FA0CE5F096', name: 'Apple Vision Pro' }, ], }, }); const executor = createCommandMatchingMockExecutor({ - whoami: { output: 'cameroncooke\n' }, + whoami: { output: 'johndoe\n' }, find: { output: `${EXAMPLE_PROJECT_PATH}\n` }, stat: { output: '1704067200\n' }, 'xcrun simctl': { output: simctlOutput }, - xcodebuild: { output: ' PRODUCT_BUNDLE_IDENTIFIER = com.example.MCPTest\n' }, + xcodebuild: { output: ' PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest\n' }, }); const result = await syncXcodeDefaultsLogic( @@ -82,33 +82,33 @@ describe('sync_xcode_defaults tool', () => { expect(result.content[0].text).toContain('Synced session defaults from Xcode IDE'); expect(result.content[0].text).toContain('Scheme: MCPTest'); expect(result.content[0].text).toContain( - 'Simulator ID: CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', + 'Simulator ID: B38FE93D-578B-454B-BE9A-C6FA0CE5F096', ); expect(result.content[0].text).toContain('Simulator Name: Apple Vision Pro'); - expect(result.content[0].text).toContain('Bundle ID: com.example.MCPTest'); + expect(result.content[0].text).toContain('Bundle ID: io.sentry.MCPTest'); const defaults = sessionStore.getAll(); expect(defaults.scheme).toBe('MCPTest'); - expect(defaults.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2'); + expect(defaults.simulatorId).toBe('B38FE93D-578B-454B-BE9A-C6FA0CE5F096'); expect(defaults.simulatorName).toBe('Apple Vision Pro'); - expect(defaults.bundleId).toBe('com.example.MCPTest'); + expect(defaults.bundleId).toBe('io.sentry.MCPTest'); }, ); it.skipIf(!existsSync(EXAMPLE_XCUSERSTATE))('syncs using configured projectPath', async () => { const simctlOutput = JSON.stringify({ devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [ - { udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', name: 'Apple Vision Pro' }, + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0': [ + { udid: 'B38FE93D-578B-454B-BE9A-C6FA0CE5F096', name: 'Apple Vision Pro' }, ], }, }); const executor = createCommandMatchingMockExecutor({ - whoami: { output: 'cameroncooke\n' }, + whoami: { output: 'johndoe\n' }, 'test -f': { success: true }, 'xcrun simctl': { output: simctlOutput }, - xcodebuild: { output: ' PRODUCT_BUNDLE_IDENTIFIER = com.example.MCPTest\n' }, + xcodebuild: { output: ' PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest\n' }, }); const result = await syncXcodeDefaultsLogic( @@ -125,8 +125,8 @@ describe('sync_xcode_defaults tool', () => { const defaults = sessionStore.getAll(); expect(defaults.scheme).toBe('MCPTest'); - expect(defaults.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2'); - expect(defaults.bundleId).toBe('com.example.MCPTest'); + expect(defaults.simulatorId).toBe('B38FE93D-578B-454B-BE9A-C6FA0CE5F096'); + expect(defaults.bundleId).toBe('io.sentry.MCPTest'); }); it.skipIf(!existsSync(EXAMPLE_XCUSERSTATE))('updates existing session defaults', async () => { @@ -139,18 +139,18 @@ describe('sync_xcode_defaults tool', () => { const simctlOutput = JSON.stringify({ devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [ - { udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', name: 'Apple Vision Pro' }, + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0': [ + { udid: 'B38FE93D-578B-454B-BE9A-C6FA0CE5F096', name: 'Apple Vision Pro' }, ], }, }); const executor = createCommandMatchingMockExecutor({ - whoami: { output: 'cameroncooke\n' }, + whoami: { output: 'johndoe\n' }, find: { output: `${EXAMPLE_PROJECT_PATH}\n` }, stat: { output: '1704067200\n' }, 'xcrun simctl': { output: simctlOutput }, - xcodebuild: { output: ' PRODUCT_BUNDLE_IDENTIFIER = com.example.MCPTest\n' }, + xcodebuild: { output: ' PRODUCT_BUNDLE_IDENTIFIER = io.sentry.MCPTest\n' }, }); const result = await syncXcodeDefaultsLogic( @@ -162,9 +162,9 @@ describe('sync_xcode_defaults tool', () => { const defaults = sessionStore.getAll(); expect(defaults.scheme).toBe('MCPTest'); - expect(defaults.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2'); + expect(defaults.simulatorId).toBe('B38FE93D-578B-454B-BE9A-C6FA0CE5F096'); expect(defaults.simulatorName).toBe('Apple Vision Pro'); - expect(defaults.bundleId).toBe('com.example.MCPTest'); + expect(defaults.bundleId).toBe('io.sentry.MCPTest'); // Original projectPath should be preserved expect(defaults.projectPath).toBe('/some/project.xcodeproj'); }); diff --git a/src/server/server.ts b/src/server/server.ts index 76cf4a68..565a048b 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -50,7 +50,7 @@ Capabilities: - SwiftPM: Build, run, test, and manage Swift Package Manager projects - Project scaffolding: Generate new iOS/macOS project templates -Only simulator workflow tools are enabled by default. If capabilities like device, macOS, debugging, or UI automation are not available, the user must configure XcodeBuildMCP to enable them. See https://github.com/cameroncooke/XcodeBuildMCP/blob/main/docs/CONFIGURATION.md for workflow configuration. +Only simulator workflow tools are enabled by default. If capabilities like device, macOS, debugging, or UI automation are not available, the user must configure XcodeBuildMCP to enable them. See https://github.com/getsentry/XcodeBuildMCP/blob/main/docs/CONFIGURATION.md for workflow configuration. Always start by calling session_show_defaults to see current configuration, then use discovery tools to find projects and set appropriate defaults.`, capabilities: { diff --git a/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts b/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts index b6ddc2f3..4642665a 100644 --- a/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts +++ b/src/smoke-tests/__tests__/e2e-mcp-device-macos.test.ts @@ -13,8 +13,8 @@ beforeAll(async () => { open: { success: true, output: '' }, kill: { success: true, output: '' }, pkill: { success: true, output: '' }, - 'defaults read': { success: true, output: 'com.example.MyApp' }, - PlistBuddy: { success: true, output: 'com.example.MyApp' }, + 'defaults read': { success: true, output: 'io.sentry.MyApp' }, + PlistBuddy: { success: true, output: 'io.sentry.MyApp' }, xcresulttool: { success: true, output: '{}' }, }, }); @@ -74,7 +74,7 @@ describe('MCP Device and macOS Tool Invocation (e2e)', () => { name: 'session_set_defaults', arguments: { deviceId: 'AAAAAAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEE', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }, }); diff --git a/src/smoke-tests/__tests__/e2e-mcp-logging.test.ts b/src/smoke-tests/__tests__/e2e-mcp-logging.test.ts index f7b628cd..3f967af6 100644 --- a/src/smoke-tests/__tests__/e2e-mcp-logging.test.ts +++ b/src/smoke-tests/__tests__/e2e-mcp-logging.test.ts @@ -25,7 +25,7 @@ describe('MCP Logging Tools (e2e)', () => { name: 'session_set_defaults', arguments: { simulatorId: 'AAAAAAAA-1111-2222-3333-444444444444', - bundleId: 'com.example.TestApp', + bundleId: 'io.sentry.TestApp', }, }); @@ -56,7 +56,7 @@ describe('MCP Logging Tools (e2e)', () => { name: 'session_set_defaults', arguments: { deviceId: 'BBBBBBBB-1111-2222-3333-444444444444', - bundleId: 'com.example.TestApp', + bundleId: 'io.sentry.TestApp', }, }); diff --git a/src/utils/__tests__/debugger-simctl.test.ts b/src/utils/__tests__/debugger-simctl.test.ts index 111d680d..615288d5 100644 --- a/src/utils/__tests__/debugger-simctl.test.ts +++ b/src/utils/__tests__/debugger-simctl.test.ts @@ -6,13 +6,13 @@ describe('resolveSimulatorAppPid', () => { it('returns PID when bundle id is found', async () => { const mockExecutor = createMockExecutor({ success: true, - output: '1234 0 com.example.MyApp\n', + output: '1234 0 io.sentry.MyApp\n', }); const pid = await resolveSimulatorAppPid({ executor: mockExecutor, simulatorId: 'SIM-123', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }); expect(pid).toBe(1234); @@ -28,7 +28,7 @@ describe('resolveSimulatorAppPid', () => { resolveSimulatorAppPid({ executor: mockExecutor, simulatorId: 'SIM-123', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }), ).rejects.toThrow('No running process found'); }); @@ -36,14 +36,14 @@ describe('resolveSimulatorAppPid', () => { it('throws when PID is missing', async () => { const mockExecutor = createMockExecutor({ success: true, - output: '- 0 com.example.MyApp\n', + output: '- 0 io.sentry.MyApp\n', }); await expect( resolveSimulatorAppPid({ executor: mockExecutor, simulatorId: 'SIM-123', - bundleId: 'com.example.MyApp', + bundleId: 'io.sentry.MyApp', }), ).rejects.toThrow('not running'); }); diff --git a/src/utils/__tests__/log_capture.test.ts b/src/utils/__tests__/log_capture.test.ts index 88761e05..32554997 100644 --- a/src/utils/__tests__/log_capture.test.ts +++ b/src/utils/__tests__/log_capture.test.ts @@ -135,7 +135,7 @@ describe('startLogCapture', () => { const fileSystem = createInMemoryFileSystemExecutor(); const result = await startLogCapture( - { simulatorUuid: 'sim-uuid', bundleId: 'com.example.app' }, + { simulatorUuid: 'sim-uuid', bundleId: 'io.sentry.app' }, executor, fileSystem, ); @@ -151,7 +151,7 @@ describe('startLogCapture', () => { 'stream', '--level=debug', '--predicate', - 'subsystem == "com.example.app"', + 'subsystem == "io.sentry.app"', ]); expect(callHistory[0].logPrefix).toBe('OS Log Capture'); expect(callHistory[0].useShell).toBe(false); @@ -166,7 +166,7 @@ describe('startLogCapture', () => { const result = await startLogCapture( { simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: 'all', }, executor, @@ -194,7 +194,7 @@ describe('startLogCapture', () => { const result = await startLogCapture( { simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: 'swiftui', }, executor, @@ -203,7 +203,7 @@ describe('startLogCapture', () => { expect(result.error).toBeUndefined(); expect(callHistory).toHaveLength(1); - expectPredicate(callHistory[0], 'com.example.app', 'swiftui'); + expectPredicate(callHistory[0], 'io.sentry.app', 'swiftui'); }); it('creates log stream command with custom subsystem predicate', async () => { @@ -214,7 +214,7 @@ describe('startLogCapture', () => { const result = await startLogCapture( { simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', subsystemFilter: ['com.apple.UIKit', 'com.apple.CoreData'], }, executor, @@ -223,7 +223,7 @@ describe('startLogCapture', () => { expect(result.error).toBeUndefined(); expect(callHistory).toHaveLength(1); - expectPredicate(callHistory[0], 'com.example.app', ['com.apple.UIKit', 'com.apple.CoreData']); + expectPredicate(callHistory[0], 'io.sentry.app', ['com.apple.UIKit', 'com.apple.CoreData']); }); it('creates console capture and log stream commands when captureConsole is true', async () => { @@ -234,7 +234,7 @@ describe('startLogCapture', () => { const result = await startLogCapture( { simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', captureConsole: true, args: ['--flag', 'value'], }, @@ -251,7 +251,7 @@ describe('startLogCapture', () => { '--console-pty', '--terminate-running-process', 'sim-uuid', - 'com.example.app', + 'io.sentry.app', '--flag', 'value', ]); @@ -272,7 +272,7 @@ describe('startLogCapture', () => { const result = await startLogCapture( { simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', captureConsole: true, env: { STAGING_ENABLED: '1', DEBUG: 'true' }, }, @@ -301,7 +301,7 @@ describe('startLogCapture', () => { const result = await startLogCapture( { simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', captureConsole: true, }, executor, @@ -349,7 +349,7 @@ describe('stopLogCapture', () => { processes: [runningProcess, finishedProcess], logFilePath, simulatorUuid: 'sim-uuid', - bundleId: 'com.example.app', + bundleId: 'io.sentry.app', logStream, }); diff --git a/src/utils/__tests__/nskeyedarchiver-parser.test.ts b/src/utils/__tests__/nskeyedarchiver-parser.test.ts index 6b04ef1b..19a74a8f 100644 --- a/src/utils/__tests__/nskeyedarchiver-parser.test.ts +++ b/src/utils/__tests__/nskeyedarchiver-parser.test.ts @@ -12,13 +12,13 @@ import { // Path to the example project's xcuserstate (used as test fixture) const EXAMPLE_PROJECT_XCUSERSTATE = join( process.cwd(), - 'example_projects/iOS/MCPTest.xcodeproj/project.xcworkspace/xcuserdata/cameroncooke.xcuserdatad/UserInterfaceState.xcuserstate', + 'example_projects/iOS/MCPTest.xcodeproj/project.xcworkspace/xcuserdata/johndoe.xcuserdatad/UserInterfaceState.xcuserstate', ); // Expected values for the MCPTest example project const EXPECTED_MCPTEST = { scheme: 'MCPTest', - simulatorId: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', + simulatorId: 'B38FE93D-578B-454B-BE9A-C6FA0CE5F096', simulatorPlatform: 'iphonesimulator', }; @@ -193,18 +193,3 @@ describe('NSKeyedArchiver Parser', () => { }); }); }); - -describe('Integration with real xcuserstate files', () => { - // Additional external test file (if available) - const HACKERNEWS_XCUSERSTATE = - '/Volumes/Developer/hackernews/ios/HackerNews.xcodeproj/project.xcworkspace/xcuserdata/cameroncooke.xcuserdatad/UserInterfaceState.xcuserstate'; - - it.skipIf(!existsSync(HACKERNEWS_XCUSERSTATE))('parses HackerNews project xcuserstate', () => { - const result = parseXcuserstate(HACKERNEWS_XCUSERSTATE); - // Scheme can vary based on user's current Xcode selection (could be any scheme in project) - expect(result.scheme).toBeDefined(); - expect(typeof result.scheme).toBe('string'); - expect(result.simulatorId).toMatch(/^[A-F0-9-]{36}$/); - expect(result.simulatorPlatform).toBe('iphonesimulator'); - }); -}); diff --git a/src/utils/__tests__/xcode-state-reader.test.ts b/src/utils/__tests__/xcode-state-reader.test.ts index 956b33c2..61ec6c9a 100644 --- a/src/utils/__tests__/xcode-state-reader.test.ts +++ b/src/utils/__tests__/xcode-state-reader.test.ts @@ -12,7 +12,7 @@ import { createCommandMatchingMockExecutor } from '../../test-utils/mock-executo const EXAMPLE_PROJECT_PATH = join(process.cwd(), 'example_projects/iOS/MCPTest.xcodeproj'); const EXAMPLE_XCUSERSTATE = join( EXAMPLE_PROJECT_PATH, - 'project.xcworkspace/xcuserdata/cameroncooke.xcuserdatad/UserInterfaceState.xcuserstate', + 'project.xcworkspace/xcuserdata/johndoe.xcuserdatad/UserInterfaceState.xcuserstate', ); describe('findXcodeStateFile', () => { @@ -236,15 +236,15 @@ describe('readXcodeIdeState integration', () => { async () => { // Mock executor that returns real paths const executor = createCommandMatchingMockExecutor({ - whoami: { output: 'cameroncooke\n' }, + whoami: { output: 'johndoe\n' }, find: { output: `${EXAMPLE_PROJECT_PATH}\n` }, stat: { output: '1704067200\n' }, 'xcrun simctl': { output: JSON.stringify({ devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [ + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0': [ { - udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', + udid: 'B38FE93D-578B-454B-BE9A-C6FA0CE5F096', name: 'Apple Vision Pro', }, ], @@ -260,7 +260,7 @@ describe('readXcodeIdeState integration', () => { expect(result.error).toBeUndefined(); expect(result.scheme).toBe('MCPTest'); - expect(result.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2'); + expect(result.simulatorId).toBe('B38FE93D-578B-454B-BE9A-C6FA0CE5F096'); expect(result.simulatorName).toBe('Apple Vision Pro'); }, ); @@ -269,14 +269,14 @@ describe('readXcodeIdeState integration', () => { 'reads scheme using configured projectPath', async () => { const executor = createCommandMatchingMockExecutor({ - whoami: { output: 'cameroncooke\n' }, + whoami: { output: 'johndoe\n' }, 'test -f': { success: true }, 'xcrun simctl': { output: JSON.stringify({ devices: { - 'com.apple.CoreSimulator.SimRuntime.iOS-18-0': [ + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0': [ { - udid: 'CE3C0D03-8F60-497A-A3B9-6A80BA997FC2', + udid: 'B38FE93D-578B-454B-BE9A-C6FA0CE5F096', name: 'Apple Vision Pro', }, ], @@ -293,7 +293,7 @@ describe('readXcodeIdeState integration', () => { expect(result.error).toBeUndefined(); expect(result.scheme).toBe('MCPTest'); - expect(result.simulatorId).toBe('CE3C0D03-8F60-497A-A3B9-6A80BA997FC2'); + expect(result.simulatorId).toBe('B38FE93D-578B-454B-BE9A-C6FA0CE5F096'); }, ); }); diff --git a/src/utils/axe-helpers.ts b/src/utils/axe-helpers.ts index c9950f2d..79a38756 100644 --- a/src/utils/axe-helpers.ts +++ b/src/utils/axe-helpers.ts @@ -117,13 +117,13 @@ export function areAxeToolsAvailable(): boolean { return getAxePath() !== null; } +export const AXE_NOT_AVAILABLE_MESSAGE = + 'AXe tool not found. UI automation features are not available.\n\n' + + 'Install AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\n' + + 'Ensure bundled artifacts are included or PATH is configured.'; + export function createAxeNotAvailableResponse(): ToolResponse { - return createTextResponse( - 'AXe tool not found. UI automation features are not available.\n\n' + - 'Install AXe (brew tap cameroncooke/axe && brew install axe) or set XCODEBUILDMCP_AXE_PATH.\n' + - 'Ensure bundled artifacts are included or PATH is configured.', - true, - ); + return createTextResponse(AXE_NOT_AVAILABLE_MESSAGE, true); } /** diff --git a/src/utils/template-manager.ts b/src/utils/template-manager.ts index 03727838..ab3b8495 100644 --- a/src/utils/template-manager.ts +++ b/src/utils/template-manager.ts @@ -11,7 +11,7 @@ import { getConfig } from './config-store.ts'; * Template manager for downloading and managing project templates */ export class TemplateManager { - private static readonly GITHUB_ORG = 'cameroncooke'; + private static readonly GITHUB_ORG = 'getsentry'; private static readonly IOS_TEMPLATE_REPO = 'XcodeBuildMCP-iOS-Template'; private static readonly MACOS_TEMPLATE_REPO = 'XcodeBuildMCP-macOS-Template'; @@ -86,7 +86,6 @@ export class TemplateManager { log('info', `Downloading ${platform} template ${version} from GitHub...`); log('info', `Download URL: ${downloadUrl}`); - // Download the release artifact const curlResult = await commandExecutor( ['curl', '-L', '-f', '-o', zipPath, downloadUrl], 'Download Template',