From d2a90a58dabf924e991810d221e5501109db3758 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Thu, 22 Jan 2026 22:49:24 +0000 Subject: [PATCH 1/6] Add tool audit option --- scripts/tools-cli.ts | 122 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 118 insertions(+), 4 deletions(-) diff --git a/scripts/tools-cli.ts b/scripts/tools-cli.ts index a8169964..523bd4ed 100644 --- a/scripts/tools-cli.ts +++ b/scripts/tools-cli.ts @@ -40,6 +40,8 @@ import * as path from 'path'; import { fileURLToPath } from 'url'; import * as fs from 'fs'; import { getStaticToolAnalysis, type StaticAnalysisResult } from './analysis/tools-analysis.js'; +import { getSchemaAuditTools } from './analysis/tools-schema-audit.js'; +import type { SchemaAuditTool } from './analysis/tools-schema-audit.js'; // Get project paths const __filename = fileURLToPath(import.meta.url); @@ -143,6 +145,7 @@ A unified command-line tool for XcodeBuildMCP tool and resource information. ${colors.bright}COMMANDS:${colors.reset} count, c Show tool and workflow counts list, l List all tools and resources + schema, audit Audit tool schemas (arguments and descriptions) static, s Show static source file analysis help, h Show this help message @@ -158,6 +161,7 @@ ${colors.bright}OPTIONS:${colors.reset} ${colors.bright}EXAMPLES:${colors.reset} ${colors.cyan}npm run tools${colors.reset} # Static summary with workflows (default) ${colors.cyan}npm run tools list${colors.reset} # List tools + ${colors.cyan}npm run tools schema${colors.reset} # Audit tool schemas ${colors.cyan}npm run tools --runtime${colors.reset} # Runtime analysis (requires build) ${colors.cyan}npm run tools static${colors.reset} # Static analysis summary ${colors.cyan}npm run tools count --json${colors.reset} # JSON output @@ -214,6 +218,36 @@ ${colors.bright}Examples:${colors.reset} ${colors.cyan}npx tsx scripts/tools-cli.ts list --static --verbose${colors.reset} # Static detailed list `, + schema: ` +${colors.bright}SCHEMA COMMAND${colors.reset} + +Audits tool schemas and prints argument descriptions (when set). + +${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts schema [options] + +${colors.bright}Options:${colors.reset} + --json Output JSON format + +${colors.bright}Examples:${colors.reset} + ${colors.cyan}npx tsx scripts/tools-cli.ts schema${colors.reset} # Human-readable schema audit + ${colors.cyan}npx tsx scripts/tools-cli.ts schema --json${colors.reset} # JSON schema audit +`, + + audit: ` +${colors.bright}SCHEMA COMMAND${colors.reset} + +Audits tool schemas and prints argument descriptions (when set). + +${colors.bright}Usage:${colors.reset} npx tsx scripts/tools-cli.ts audit [options] + +${colors.bright}Options:${colors.reset} + --json Output JSON format + +${colors.bright}Examples:${colors.reset} + ${colors.cyan}npx tsx scripts/tools-cli.ts audit${colors.reset} # Human-readable schema audit + ${colors.cyan}npx tsx scripts/tools-cli.ts audit --json${colors.reset} # JSON schema audit +`, + static: ` ${colors.bright}STATIC COMMAND${colors.reset} @@ -573,6 +607,65 @@ function outputJSON( console.log(JSON.stringify(output, null, 2)); } +function isSchemaAuditCommand(commandName: string): boolean { + return commandName === 'schema' || commandName === 'audit'; +} + +function displaySchemaAudit(tools: SchemaAuditTool[]): void { + if (options.json) { + return; + } + + console.log(`${colors.bright}Tool Schema Audit:${colors.reset}`); + console.log('โ”€'.repeat(60)); + + if (tools.length === 0) { + console.log('No tools found.'); + console.log(); + return; + } + + for (const tool of tools) { + const toolDescription = tool.description ?? 'No description provided'; + console.log(`Tool Name: ${tool.name}`); + console.log(`Tool Description: ${toolDescription}`); + console.log(`Arguments (${tool.args.length}):`); + + if (tool.args.length === 0) { + console.log(' (none)'); + } else { + for (const arg of tool.args) { + const argDescription = arg.description ?? 'No description provided'; + console.log(` Argument Name: ${arg.name}`); + console.log(` Argument Description: ${argDescription}`); + } + } + + console.log(); + } +} + +function outputSchemaAuditJSON(tools: SchemaAuditTool[]): void { + console.log( + JSON.stringify( + { + mode: 'schema-audit', + toolCount: tools.length, + tools: tools.map((tool) => ({ + name: tool.name, + description: tool.description, + args: tool.args.map((arg) => ({ + name: arg.name, + description: arg.description, + })), + })), + }, + null, + 2, + ), + ); +} + /** * Main execution function */ @@ -580,16 +673,18 @@ async function main(): Promise { try { let runtimeData: RuntimeData | null = null; let staticData: StaticAnalysisResult | null = null; + let schemaAuditTools: SchemaAuditTool[] | null = null; + const schemaAuditCommand = isSchemaAuditCommand(command); // Gather data based on options - if (options.runtime) { + if (options.runtime && !schemaAuditCommand) { if (!options.json) { console.log(`${colors.cyan}๐Ÿ” Gathering runtime information...${colors.reset}`); } runtimeData = await getRuntimeInfo(); } - if (options.static) { + if (options.static && !schemaAuditCommand) { if (!options.json) { console.log(`${colors.cyan}๐Ÿ“ Performing static analysis...${colors.reset}`); } @@ -597,20 +692,31 @@ async function main(): Promise { } // For default command or workflows option, always gather static data for workflow info - if (options.workflows && !staticData) { + if (options.workflows && !staticData && !schemaAuditCommand) { if (!options.json) { console.log(`${colors.cyan}๐Ÿ“ Gathering workflow information...${colors.reset}`); } staticData = await getStaticToolAnalysis(); } + if (schemaAuditCommand) { + if (!options.json) { + console.log(`${colors.cyan}Gathering tool schema details...${colors.reset}`); + } + schemaAuditTools = await getSchemaAuditTools(); + } + if (!options.json) { console.log(); // Blank line after gathering } // Handle JSON output if (options.json) { - outputJSON(runtimeData, staticData); + if (schemaAuditCommand) { + outputSchemaAuditJSON(schemaAuditTools ?? []); + } else { + outputJSON(runtimeData, staticData); + } return; } @@ -652,6 +758,14 @@ async function main(): Promise { } break; + case 'schema': + case 'audit': + if (!schemaAuditTools) { + schemaAuditTools = await getSchemaAuditTools(); + } + displaySchemaAudit(schemaAuditTools); + break; + default: // Default case (no command) - show runtime summary with workflows displaySummary(runtimeData, staticData); From a05e294563adbfb9384fb61d8eb4188a290cb347 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Fri, 23 Jan 2026 00:10:37 +0000 Subject: [PATCH 2/6] More tool descriptions more concise --- docs/dev/tools_cli_schema_audit_plan.md | 100 ++ docs/dev/tools_schema_redundancy.md | 68 + scripts/analysis/tools-schema-audit.ts | 484 +++++++ src/mcp/tools/debugging/debug_attach_sim.ts | 18 +- .../tools/debugging/debug_breakpoint_add.ts | 15 +- .../debugging/debug_breakpoint_remove.ts | 9 +- src/mcp/tools/debugging/debug_continue.ts | 7 +- src/mcp/tools/debugging/debug_detach.ts | 7 +- src/mcp/tools/debugging/debug_lldb_command.ts | 26 +- src/mcp/tools/debugging/debug_stack.ts | 11 +- src/mcp/tools/debugging/debug_variables.ts | 9 +- .../device/__tests__/build_device.test.ts | 2 +- .../device/__tests__/test_device.test.ts | 2 +- src/mcp/tools/device/build_device.ts | 8 +- src/mcp/tools/device/get_device_app_path.ts | 7 +- src/mcp/tools/device/install_app_device.ts | 6 +- src/mcp/tools/device/launch_app_device.ts | 6 +- src/mcp/tools/device/list_devices.ts | 3 +- src/mcp/tools/device/stop_app_device.ts | 4 +- src/mcp/tools/device/test_device.ts | 13 +- src/mcp/tools/doctor/doctor.ts | 5 +- .../__tests__/start_device_log_cap.test.ts | 2 +- .../__tests__/start_sim_log_cap.test.ts | 4 +- src/mcp/tools/logging/start_device_log_cap.ts | 4 +- src/mcp/tools/logging/start_sim_log_cap.ts | 14 +- src/mcp/tools/logging/stop_device_log_cap.ts | 4 +- src/mcp/tools/logging/stop_sim_log_cap.ts | 4 +- .../macos/__tests__/get_mac_app_path.test.ts | 2 +- .../tools/macos/__tests__/test_macos.test.ts | 2 +- src/mcp/tools/macos/build_macos.ts | 14 +- src/mcp/tools/macos/build_run_macos.ts | 14 +- src/mcp/tools/macos/get_mac_app_path.ts | 6 +- src/mcp/tools/macos/launch_mac_app.ts | 9 +- src/mcp/tools/macos/stop_mac_app.ts | 9 +- src/mcp/tools/macos/test_macos.ts | 14 +- .../__tests__/get_app_bundle_id.test.ts | 4 +- .../__tests__/get_mac_bundle_id.test.ts | 4 +- .../__tests__/show_build_settings.test.ts | 2 +- .../tools/project-discovery/discover_projs.ts | 14 +- .../project-discovery/get_app_bundle_id.ts | 9 +- .../project-discovery/get_mac_bundle_id.ts | 9 +- .../tools/project-discovery/list_schemes.ts | 2 +- .../project-discovery/show_build_settings.ts | 2 +- .../__tests__/scaffold_ios_project.test.ts | 4 +- .../scaffold_ios_project.ts | 52 +- .../scaffold_macos_project.ts | 41 +- .../session_clear_defaults.ts | 2 +- .../session_set_defaults.ts | 54 +- .../session_show_defaults.ts | 2 +- .../__tests__/erase_sims.test.ts | 2 +- .../__tests__/reset_sim_location.test.ts | 4 +- .../__tests__/set_sim_appearance.test.ts | 4 +- .../__tests__/set_sim_location.test.ts | 2 +- .../__tests__/sim_statusbar.test.ts | 4 +- .../tools/simulator-management/erase_sims.ts | 7 +- .../reset_sim_location.ts | 2 +- .../set_sim_appearance.ts | 4 +- .../simulator-management/set_sim_location.ts | 6 +- .../simulator-management/sim_statusbar.ts | 7 +- .../simulator/__tests__/build_run_sim.test.ts | 2 +- .../simulator/__tests__/build_sim.test.ts | 2 +- .../__tests__/get_sim_app_path.test.ts | 2 +- .../__tests__/install_app_sim.test.ts | 2 +- .../__tests__/launch_app_logs_sim.test.ts | 4 +- .../__tests__/launch_app_sim.test.ts | 2 +- .../simulator/__tests__/list_sims.test.ts | 2 +- .../simulator/__tests__/open_sim.test.ts | 2 +- .../simulator/__tests__/screenshot.test.ts | 4 +- .../simulator/__tests__/stop_app_sim.test.ts | 2 +- .../simulator/__tests__/test_sim.test.ts | 2 +- src/mcp/tools/simulator/boot_sim.ts | 2 +- src/mcp/tools/simulator/build_run_sim.ts | 16 +- src/mcp/tools/simulator/build_sim.ts | 16 +- src/mcp/tools/simulator/get_sim_app_path.ts | 6 +- src/mcp/tools/simulator/install_app_sim.ts | 6 +- .../tools/simulator/launch_app_logs_sim.ts | 8 +- src/mcp/tools/simulator/launch_app_sim.ts | 8 +- src/mcp/tools/simulator/list_sims.ts | 4 +- src/mcp/tools/simulator/open_sim.ts | 2 +- src/mcp/tools/simulator/record_sim_video.ts | 13 +- src/mcp/tools/simulator/stop_app_sim.ts | 4 +- src/mcp/tools/simulator/test_sim.ts | 16 +- .../__tests__/swift_package_build.test.ts | 2 +- .../__tests__/swift_package_clean.test.ts | 4 +- .../__tests__/swift_package_list.test.ts | 2 +- .../__tests__/swift_package_run.test.ts | 4 +- .../__tests__/swift_package_stop.test.ts | 4 +- .../__tests__/swift_package_test.test.ts | 2 +- .../swift-package/swift_package_build.ts | 15 +- .../swift-package/swift_package_clean.ts | 4 +- .../tools/swift-package/swift_package_list.ts | 2 +- .../tools/swift-package/swift_package_run.ts | 28 +- .../tools/swift-package/swift_package_stop.ts | 4 +- .../tools/swift-package/swift_package_test.ts | 22 +- .../tools/ui-testing/__tests__/button.test.ts | 4 +- .../ui-testing/__tests__/describe_ui.test.ts | 2 +- .../ui-testing/__tests__/gesture.test.ts | 4 +- .../ui-testing/__tests__/key_press.test.ts | 4 +- .../ui-testing/__tests__/key_sequence.test.ts | 4 +- .../ui-testing/__tests__/long_press.test.ts | 4 +- .../ui-testing/__tests__/screenshot.test.ts | 4 +- .../tools/ui-testing/__tests__/swipe.test.ts | 4 +- .../tools/ui-testing/__tests__/tap.test.ts | 4 +- .../tools/ui-testing/__tests__/touch.test.ts | 4 +- .../ui-testing/__tests__/type_text.test.ts | 4 +- src/mcp/tools/ui-testing/button.ts | 13 +- src/mcp/tools/ui-testing/describe_ui.ts | 2 +- src/mcp/tools/ui-testing/gesture.ts | 17 +- src/mcp/tools/ui-testing/key_press.ts | 16 +- src/mcp/tools/ui-testing/key_sequence.ts | 5 +- src/mcp/tools/ui-testing/long_press.ts | 8 +- src/mcp/tools/ui-testing/screenshot.ts | 3 +- src/mcp/tools/ui-testing/swipe.ts | 21 +- src/mcp/tools/ui-testing/tap.ts | 15 +- src/mcp/tools/ui-testing/touch.ts | 9 +- src/mcp/tools/ui-testing/type_text.ts | 3 +- .../tools/utilities/__tests__/clean.test.ts | 2 +- src/mcp/tools/utilities/clean.ts | 21 +- tools.compact.json | 1146 ++++++++++++++++ tools.new.json | 1150 +++++++++++++++++ tools_original.json | 1146 ++++++++++++++++ 121 files changed, 4414 insertions(+), 580 deletions(-) create mode 100644 docs/dev/tools_cli_schema_audit_plan.md create mode 100644 docs/dev/tools_schema_redundancy.md create mode 100644 scripts/analysis/tools-schema-audit.ts create mode 100644 tools.compact.json create mode 100644 tools.new.json create mode 100644 tools_original.json diff --git a/docs/dev/tools_cli_schema_audit_plan.md b/docs/dev/tools_cli_schema_audit_plan.md new file mode 100644 index 00000000..2065e6f9 --- /dev/null +++ b/docs/dev/tools_cli_schema_audit_plan.md @@ -0,0 +1,100 @@ +# 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 new file mode 100644 index 00000000..7c379141 --- /dev/null +++ b/docs/dev/tools_schema_redundancy.md @@ -0,0 +1,68 @@ +# Tools Schema Session-Default Audit + +This document identifies arguments in `tools.compact.json` that should be removed from individual tool schemas because they are typically set once per session. It also provides a concrete checklist of tool-level removals and default additions. + +## Session defaults to add or reinforce + +- derivedDataPath: Add to session defaults; remove from build/test/clean tool schemas. +- preferXcodebuild: Add to session defaults; remove from build/test/clean tool schemas. +- configuration (SwiftPM): Add to SwiftPM session defaults; applies to Swift Package build/run/test tools only. +- platform: Add to device session defaults; applies to device-only tools (get_device_app_path, test_device). + +## Optional session defaults (sticky per workflow) + +- bundleId: Consider as a session default for launch/stop/log tools when working on a single app. + +## Removal checklist by tool + +### Build and test (Xcode) + +Remove derivedDataPath, preferXcodebuild from: + +- build_device +- build_sim +- build_run_sim +- build_macos +- build_run_macos +- test_device +- test_sim +- test_macos +- clean + +Remove platform from: + +- get_device_app_path +- test_device + +### Swift Package Manager + +Remove configuration (if promoted to session default) from: + +- swift_package_build +- swift_package_run +- swift_package_test + +### Launch/stop/log (bundleId default) + +Remove bundleId (if promoted to session default) from: + +- launch_app_device +- launch_app_sim +- launch_app_logs_sim +- stop_app_sim +- start_device_log_cap +- 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 + +When removing arguments from individual schemas, ensure tool descriptions mention: + +- The session default that will be used. +- How to override per call (if overrides remain supported). diff --git a/scripts/analysis/tools-schema-audit.ts b/scripts/analysis/tools-schema-audit.ts new file mode 100644 index 00000000..87bf751c --- /dev/null +++ b/scripts/analysis/tools-schema-audit.ts @@ -0,0 +1,484 @@ +#!/usr/bin/env node + +/** + * Tool schema audit analysis for XcodeBuildMCP. + * + * Static analysis of tool files to extract schema argument names and their + * `.describe()` strings without loading tool modules at runtime. + */ + +import { + createSourceFile, + forEachChild, + isAsExpression, + isCallExpression, + isExportAssignment, + isIdentifier, + isNoSubstitutionTemplateLiteral, + isObjectLiteralExpression, + isParenthesizedExpression, + isPropertyAccessExpression, + isPropertyAssignment, + isShorthandPropertyAssignment, + isSpreadAssignment, + isStringLiteral, + isTemplateExpression, + isVariableDeclaration, + isVariableStatement, + type Expression, + type Node, + type ObjectLiteralExpression, + ScriptTarget, + type SourceFile, +} from 'typescript'; +import * as fs from 'fs'; +import * as path from 'path'; +import { glob } from 'glob'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const projectRoot = path.resolve(__dirname, '..', '..'); +const toolsDir = path.join(projectRoot, 'src', 'mcp', 'tools'); + +export interface SchemaAuditArgument { + name: string; + description: string | null; +} + +export interface SchemaAuditTool { + name: string; + description: string | null; + args: SchemaAuditArgument[]; + relativePath: string; +} + +type SchemaShape = Map; + +function isReExportFile(filePath: string): boolean { + const content = fs.readFileSync(filePath, 'utf-8'); + const contentWithoutBlockComments = content.replace(/\/\*[\s\S]*?\*\//g, ''); + const cleanedLines = contentWithoutBlockComments + .split('\n') + .map((line) => line.split('//')[0].trim()) + .filter((line) => line.length > 0); + + if (cleanedLines.length !== 1) { + return false; + } + + const exportLine = cleanedLines[0]; + return /^export\s*{\s*default\s*}\s*from\s*['"][^'"]+['"];?\s*$/.test(exportLine); +} + +function extractStringLiteral( + expression: Expression | undefined, + sourceFile: SourceFile, +): string | null { + if (!expression) { + return null; + } + + if (isStringLiteral(expression)) { + return expression.text.trim(); + } + + if (isNoSubstitutionTemplateLiteral(expression)) { + return expression.text.trim(); + } + + if (isTemplateExpression(expression)) { + const raw = expression.getFullText(sourceFile).trim(); + if (raw.startsWith('`') && raw.endsWith('`')) { + return raw.slice(1, -1).trim(); + } + return raw; + } + + return null; +} + +function getPropertyName(node: Node): string | null { + if (isIdentifier(node)) { + return node.text; + } + if (isStringLiteral(node)) { + return node.text; + } + return null; +} + +function extractDescribeCall(expression: Expression, sourceFile: SourceFile): string | null { + if (isCallExpression(expression)) { + if ( + isPropertyAccessExpression(expression.expression) && + expression.expression.name.text === 'describe' + ) { + return extractStringLiteral(expression.arguments[0], sourceFile); + } + + const fromCallee = extractDescribeCall(expression.expression, sourceFile); + if (fromCallee) { + return fromCallee; + } + } + + if (isPropertyAccessExpression(expression)) { + return extractDescribeCall(expression.expression, sourceFile); + } + + if (isParenthesizedExpression(expression)) { + return extractDescribeCall(expression.expression, sourceFile); + } + + if (isAsExpression(expression)) { + return extractDescribeCall(expression.expression, sourceFile); + } + + return null; +} + +function resolveObjectLiteralShape( + objectLiteral: ObjectLiteralExpression, + context: SchemaResolveContext, +): SchemaShape { + const shape: SchemaShape = new Map(); + + for (const property of objectLiteral.properties) { + if (isPropertyAssignment(property)) { + const name = getPropertyName(property.name); + if (!name) { + continue; + } + + const description = extractDescribeCall(property.initializer, context.sourceFile); + shape.set(name, description ?? null); + continue; + } + + if (isShorthandPropertyAssignment(property)) { + const name = property.name.text; + if (!name) { + continue; + } + + shape.set(name, null); + continue; + } + + if (isSpreadAssignment(property)) { + const spreadShape = resolveSchemaShape(property.expression, context); + for (const [key, value] of spreadShape.entries()) { + shape.set(key, value); + } + } + } + + return shape; +} + +function isZodCall(expression: Expression, name: string): boolean { + if (!isCallExpression(expression)) { + return false; + } + if (!isPropertyAccessExpression(expression.expression)) { + return false; + } + if (!isIdentifier(expression.expression.expression)) { + return false; + } + + return expression.expression.expression.text === 'z' && expression.expression.name.text === name; +} + +function resolveOmitKeys(expression: Expression): Set { + if (isAsExpression(expression)) { + return resolveOmitKeys(expression.expression); + } + + if (!isObjectLiteralExpression(expression)) { + return new Set(); + } + + const keys = new Set(); + for (const property of expression.properties) { + if (isPropertyAssignment(property)) { + const name = getPropertyName(property.name); + if (name) { + keys.add(name); + } + } + } + return keys; +} + +function resolveSchemaShape(expression: Expression, context: SchemaResolveContext): SchemaShape { + if (isParenthesizedExpression(expression)) { + return resolveSchemaShape(expression.expression, context); + } + + if (isAsExpression(expression)) { + return resolveSchemaShape(expression.expression, context); + } + + if (isIdentifier(expression)) { + const name = expression.text; + if (context.memo.has(name)) { + return context.memo.get(name) ?? new Map(); + } + if (context.resolving.has(name)) { + return new Map(); + } + + const initializer = context.variableInitializers.get(name); + if (!initializer) { + return new Map(); + } + + context.resolving.add(name); + const resolved = resolveSchemaShape(initializer, context); + context.memo.set(name, resolved); + context.resolving.delete(name); + return resolved; + } + + if (isObjectLiteralExpression(expression)) { + return resolveObjectLiteralShape(expression, context); + } + + if (isPropertyAccessExpression(expression) && expression.name.text === 'shape') { + return resolveSchemaShape(expression.expression, context); + } + + if (isCallExpression(expression)) { + if (isZodCall(expression, 'object') || isZodCall(expression, 'strictObject')) { + const firstArg = expression.arguments[0]; + if (firstArg) { + return resolveSchemaShape(firstArg, context); + } + } + + if (isZodCall(expression, 'preprocess')) { + const schemaArg = expression.arguments[1]; + if (schemaArg) { + return resolveSchemaShape(schemaArg, context); + } + } + + if (isIdentifier(expression.expression) && expression.expression.text === 'getSessionAwareToolSchemaShape') { + const firstArg = expression.arguments[0]; + if (firstArg && isObjectLiteralExpression(firstArg)) { + let sessionAware: Expression | null = null; + let legacy: Expression | null = null; + + for (const property of firstArg.properties) { + if (isPropertyAssignment(property) && isIdentifier(property.name)) { + if (property.name.text === 'sessionAware') { + sessionAware = property.initializer; + } else if (property.name.text === 'legacy') { + legacy = property.initializer; + } + } + } + + if (sessionAware) { + return resolveSchemaShape(sessionAware, context); + } + if (legacy) { + return resolveSchemaShape(legacy, context); + } + } + } + + if (isPropertyAccessExpression(expression.expression)) { + const operation = expression.expression.name.text; + const baseExpression = expression.expression.expression; + const baseShape = resolveSchemaShape(baseExpression, context); + + if (operation === 'omit') { + const omitKeys = resolveOmitKeys(expression.arguments[0]); + for (const key of omitKeys) { + baseShape.delete(key); + } + return baseShape; + } + + if (operation === 'extend') { + const extensionArg = expression.arguments[0]; + if (extensionArg) { + const extensionShape = resolveSchemaShape(extensionArg, context); + for (const [key, value] of extensionShape.entries()) { + baseShape.set(key, value); + } + } + return baseShape; + } + + if (operation === 'passthrough') { + return baseShape; + } + } + } + + return new Map(); +} + +interface SchemaResolveContext { + sourceFile: SourceFile; + variableInitializers: Map; + memo: Map; + resolving: Set; +} + +function getExportDefaultObject(sourceFile: SourceFile): ObjectLiteralExpression | null { + let exportObject: ObjectLiteralExpression | null = null; + + function visit(node: Node): void { + if (isExportAssignment(node) && !node.isExportEquals) { + if (isObjectLiteralExpression(node.expression)) { + exportObject = node.expression; + return; + } + } + + forEachChild(node, visit); + } + + visit(sourceFile); + return exportObject; +} + +function extractToolMetadata( + sourceFile: SourceFile, + variableInitializers: Map, + fallbackName: string, +): { name: string; description: string | null; schemaExpression: Expression } { + const exportObject = getExportDefaultObject(sourceFile); + if (!exportObject) { + throw new Error('Export default object not found.'); + } + + let name: string | null = null; + let description: string | null = null; + let schemaExpression: Expression | null = null; + + for (const property of exportObject.properties) { + if (!isPropertyAssignment(property) || !isIdentifier(property.name)) { + continue; + } + + if (property.name.text === 'name') { + name = extractStringLiteral(property.initializer, sourceFile); + } else if (property.name.text === 'description') { + description = extractStringLiteral(property.initializer, sourceFile); + } else if (property.name.text === 'schema') { + schemaExpression = property.initializer; + } + } + + if (!schemaExpression) { + throw new Error('Tool schema not found.'); + } + + return { + name: name ?? fallbackName, + description, + schemaExpression, + }; +} + +function collectVariableInitializers(sourceFile: SourceFile): Map { + const map = new Map(); + + function visit(node: Node): void { + if (isVariableStatement(node)) { + for (const declaration of node.declarationList.declarations) { + if ( + isVariableDeclaration(declaration) && + isIdentifier(declaration.name) && + declaration.initializer + ) { + map.set(declaration.name.text, declaration.initializer); + } + } + } + + forEachChild(node, visit); + } + + visit(sourceFile); + return map; +} + +export async function getSchemaAuditTools(): Promise { + const files = await glob('**/*.ts', { + cwd: toolsDir, + ignore: [ + '**/__tests__/**', + '**/index.ts', + '**/*.test.ts', + '**/lib/**', + '**/*-processes.ts', + '**/*.deps.ts', + '**/*-utils.ts', + '**/*-common.ts', + '**/*-types.ts', + ], + absolute: true, + }); + + const tools: SchemaAuditTool[] = []; + + for (const filePath of files) { + if (isReExportFile(filePath)) { + continue; + } + + const content = fs.readFileSync(filePath, 'utf-8'); + const sourceFile = createSourceFile(filePath, content, ScriptTarget.Latest, true); + const variableInitializers = collectVariableInitializers(sourceFile); + + const toolNameFallback = path.basename(filePath, '.ts'); + const { name, description, schemaExpression } = extractToolMetadata( + sourceFile, + variableInitializers, + toolNameFallback, + ); + + const context: SchemaResolveContext = { + sourceFile, + variableInitializers, + memo: new Map(), + resolving: new Set(), + }; + + const shape = resolveSchemaShape(schemaExpression, context); + const args = Array.from(shape.entries()) + .map(([argName, argDescription]) => ({ + name: argName, + description: argDescription, + })) + .sort((a, b) => a.name.localeCompare(b.name)); + + tools.push({ + name, + description, + args, + relativePath: path.relative(projectRoot, filePath), + }); + } + + return tools.sort((a, b) => a.name.localeCompare(b.name)); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + async function main(): Promise { + const tools = await getSchemaAuditTools(); + console.log(JSON.stringify(tools, null, 2)); + } + + main().catch((error) => { + console.error('Schema audit failed:', error); + process.exit(1); + }); +} diff --git a/src/mcp/tools/debugging/debug_attach_sim.ts b/src/mcp/tools/debugging/debug_attach_sim.ts index bda3ec15..a1de791b 100644 --- a/src/mcp/tools/debugging/debug_attach_sim.ts +++ b/src/mcp/tools/debugging/debug_attach_sim.ts @@ -27,22 +27,15 @@ const baseSchemaObject = z.object({ .describe( "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both", ), - bundleId: z - .string() - .optional() - .describe("Bundle identifier of the app to attach (e.g., 'com.example.MyApp')"), - pid: z.number().int().positive().optional().describe('Process ID to attach directly'), + bundleId: z.string().optional(), + pid: z.number().int().positive().optional(), waitFor: z.boolean().optional().describe('Wait for the process to appear when attaching'), - continueOnAttach: z - .boolean() - .optional() - .default(true) - .describe('Resume execution automatically after attaching (default: true)'), + continueOnAttach: z.boolean().optional().default(true).describe('default: true'), makeCurrent: z .boolean() .optional() .default(true) - .describe('Set this debug session as the current session (default: true)'), + .describe('Set debug session as current (default: true)'), }); const debugAttachSchema = z.preprocess( @@ -167,8 +160,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'debug_attach_sim', - description: - 'Attach LLDB to a running iOS simulator app. Provide bundleId or pid, plus simulator defaults.', + description: 'Attach LLDB to sim app.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/debugging/debug_breakpoint_add.ts b/src/mcp/tools/debugging/debug_breakpoint_add.ts index 0c39adeb..fdd817c1 100644 --- a/src/mcp/tools/debugging/debug_breakpoint_add.ts +++ b/src/mcp/tools/debugging/debug_breakpoint_add.ts @@ -10,14 +10,11 @@ import { } from '../../../utils/debugger/index.ts'; const baseSchemaObject = z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to target (defaults to current session)'), - file: z.string().optional().describe('Source file path for breakpoint'), - line: z.number().int().positive().optional().describe('Line number for breakpoint'), - function: z.string().optional().describe('Function name to break on'), - condition: z.string().optional().describe('Optional condition expression for the breakpoint'), + debugSessionId: z.string().optional().describe('default: current session'), + file: z.string().optional(), + line: z.number().int().positive().optional(), + function: z.string().optional(), + condition: z.string().optional().describe('Expression for breakpoint condition'), }); const debugBreakpointAddSchema = z.preprocess( @@ -58,7 +55,7 @@ export async function debug_breakpoint_addLogic( export default { name: 'debug_breakpoint_add', - description: 'Add a breakpoint by file/line or function name for the active debug session.', + description: 'Add breakpoint.', schema: baseSchemaObject.shape, handler: createTypedToolWithContext( debugBreakpointAddSchema as unknown as z.ZodType, diff --git a/src/mcp/tools/debugging/debug_breakpoint_remove.ts b/src/mcp/tools/debugging/debug_breakpoint_remove.ts index a4ff1de2..5e03477e 100644 --- a/src/mcp/tools/debugging/debug_breakpoint_remove.ts +++ b/src/mcp/tools/debugging/debug_breakpoint_remove.ts @@ -8,11 +8,8 @@ import { } from '../../../utils/debugger/index.ts'; const debugBreakpointRemoveSchema = z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to target (defaults to current session)'), - breakpointId: z.number().int().positive().describe('Breakpoint id to remove'), + debugSessionId: z.string().optional().describe('default: current session'), + breakpointId: z.number().int().positive(), }); export type DebugBreakpointRemoveParams = z.infer; @@ -32,7 +29,7 @@ export async function debug_breakpoint_removeLogic( export default { name: 'debug_breakpoint_remove', - description: 'Remove a breakpoint by id for the active debug session.', + description: 'Remove breakpoint.', schema: debugBreakpointRemoveSchema.shape, handler: createTypedToolWithContext( debugBreakpointRemoveSchema, diff --git a/src/mcp/tools/debugging/debug_continue.ts b/src/mcp/tools/debugging/debug_continue.ts index c63092ea..e7cc7f16 100644 --- a/src/mcp/tools/debugging/debug_continue.ts +++ b/src/mcp/tools/debugging/debug_continue.ts @@ -8,10 +8,7 @@ import { } from '../../../utils/debugger/index.ts'; const debugContinueSchema = z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to resume (defaults to current session)'), + debugSessionId: z.string().optional().describe('default: current session'), }); export type DebugContinueParams = z.infer; @@ -33,7 +30,7 @@ export async function debug_continueLogic( export default { name: 'debug_continue', - description: 'Resume execution in the active debug session or a specific debugSessionId.', + description: 'Continue debug session.', schema: debugContinueSchema.shape, handler: createTypedToolWithContext( debugContinueSchema, diff --git a/src/mcp/tools/debugging/debug_detach.ts b/src/mcp/tools/debugging/debug_detach.ts index 4e868383..25a568c7 100644 --- a/src/mcp/tools/debugging/debug_detach.ts +++ b/src/mcp/tools/debugging/debug_detach.ts @@ -8,10 +8,7 @@ import { } from '../../../utils/debugger/index.ts'; const debugDetachSchema = z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to detach (defaults to current session)'), + debugSessionId: z.string().optional().describe('default: current session'), }); export type DebugDetachParams = z.infer; @@ -33,7 +30,7 @@ export async function debug_detachLogic( export default { name: 'debug_detach', - description: 'Detach the current debugger session or a specific debugSessionId.', + description: 'Detach debugger.', schema: debugDetachSchema.shape, handler: createTypedToolWithContext( debugDetachSchema, diff --git a/src/mcp/tools/debugging/debug_lldb_command.ts b/src/mcp/tools/debugging/debug_lldb_command.ts index 6fae6160..6368e273 100644 --- a/src/mcp/tools/debugging/debug_lldb_command.ts +++ b/src/mcp/tools/debugging/debug_lldb_command.ts @@ -8,17 +8,13 @@ import { type DebuggerToolContext, } from '../../../utils/debugger/index.ts'; -const debugLldbCommandSchema = z.preprocess( - nullifyEmptyStrings, - z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to target (defaults to current session)'), - command: z.string().describe('LLDB command to run (e.g., "continue", "thread backtrace")'), - timeoutMs: z.number().int().positive().optional().describe('Override command timeout (ms)'), - }), -); +const baseSchemaObject = z.object({ + debugSessionId: z.string().optional().describe('default: current session'), + command: z.string(), + timeoutMs: z.number().int().positive().optional(), +}); + +const debugLldbCommandSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject); export type DebugLldbCommandParams = z.infer; @@ -39,12 +35,8 @@ export async function debug_lldb_commandLogic( export default { name: 'debug_lldb_command', - description: 'Run an arbitrary LLDB command within the active debug session.', - schema: z.object({ - debugSessionId: z.string().optional(), - command: z.string(), - timeoutMs: z.number().int().positive().optional(), - }).shape, + description: 'Run LLDB command.', + schema: baseSchemaObject.shape, handler: createTypedToolWithContext( debugLldbCommandSchema as unknown as z.ZodType, debug_lldb_commandLogic, diff --git a/src/mcp/tools/debugging/debug_stack.ts b/src/mcp/tools/debugging/debug_stack.ts index ddfb2899..90e06f4d 100644 --- a/src/mcp/tools/debugging/debug_stack.ts +++ b/src/mcp/tools/debugging/debug_stack.ts @@ -8,12 +8,9 @@ import { } from '../../../utils/debugger/index.ts'; const debugStackSchema = z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to target (defaults to current session)'), - threadIndex: z.number().int().nonnegative().optional().describe('Thread index for backtrace'), - maxFrames: z.number().int().positive().optional().describe('Maximum frames to return'), + debugSessionId: z.string().optional().describe('default: current session'), + threadIndex: z.number().int().nonnegative().optional(), + maxFrames: z.number().int().positive().optional(), }); export type DebugStackParams = z.infer; @@ -36,7 +33,7 @@ export async function debug_stackLogic( export default { name: 'debug_stack', - description: 'Return a thread backtrace from the active debug session.', + description: 'Get backtrace.', schema: debugStackSchema.shape, handler: createTypedToolWithContext( debugStackSchema, diff --git a/src/mcp/tools/debugging/debug_variables.ts b/src/mcp/tools/debugging/debug_variables.ts index 2442dbdb..18b7f820 100644 --- a/src/mcp/tools/debugging/debug_variables.ts +++ b/src/mcp/tools/debugging/debug_variables.ts @@ -8,11 +8,8 @@ import { } from '../../../utils/debugger/index.ts'; const debugVariablesSchema = z.object({ - debugSessionId: z - .string() - .optional() - .describe('Debug session ID to target (defaults to current session)'), - frameIndex: z.number().int().nonnegative().optional().describe('Frame index to inspect'), + debugSessionId: z.string().optional().describe('default: current session'), + frameIndex: z.number().int().nonnegative().optional(), }); export type DebugVariablesParams = z.infer; @@ -34,7 +31,7 @@ export async function debug_variablesLogic( export default { name: 'debug_variables', - description: 'Return variables for a selected frame in the active debug session.', + description: 'Get frame variables.', schema: debugVariablesSchema.shape, handler: createTypedToolWithContext( debugVariablesSchema, diff --git a/src/mcp/tools/device/__tests__/build_device.test.ts b/src/mcp/tools/device/__tests__/build_device.test.ts index 254ca35b..2b55248a 100644 --- a/src/mcp/tools/device/__tests__/build_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_device.test.ts @@ -25,7 +25,7 @@ describe('build_device plugin', () => { }); it('should have correct description', () => { - expect(buildDevice.description).toBe('Builds an app for a connected device.'); + expect(buildDevice.description).toBe('Build for device.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/device/__tests__/test_device.test.ts b/src/mcp/tools/device/__tests__/test_device.test.ts index d5da3c9d..a07486e0 100644 --- a/src/mcp/tools/device/__tests__/test_device.test.ts +++ b/src/mcp/tools/device/__tests__/test_device.test.ts @@ -26,7 +26,7 @@ describe('test_device plugin', () => { }); it('should have correct description', () => { - expect(testDevice.description).toBe('Runs tests on a physical Apple device.'); + expect(testDevice.description).toBe('Test on device.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/device/build_device.ts b/src/mcp/tools/device/build_device.ts index d4bb6cc9..5ba4f399 100644 --- a/src/mcp/tools/device/build_device.ts +++ b/src/mcp/tools/device/build_device.ts @@ -22,9 +22,9 @@ const baseSchemaObject = z.object({ workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), scheme: z.string().describe('The scheme to build'), configuration: z.string().optional().describe('Build configuration (Debug, Release)'), - derivedDataPath: z.string().optional().describe('Path to derived data directory'), - extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'), - preferXcodebuild: z.boolean().optional().describe('Prefer xcodebuild over faster alternatives'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), + preferXcodebuild: z.boolean().optional(), }); const buildDeviceSchema = z.preprocess( @@ -74,7 +74,7 @@ export async function buildDeviceLogic( export default { name: 'build_device', - description: 'Builds an app for a connected device.', + description: 'Build for device.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/device/get_device_app_path.ts b/src/mcp/tools/device/get_device_app_path.ts index 63937333..7968048c 100644 --- a/src/mcp/tools/device/get_device_app_path.ts +++ b/src/mcp/tools/device/get_device_app_path.ts @@ -21,10 +21,7 @@ import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; const baseOptions = { scheme: z.string().describe('The scheme to use'), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - platform: z - .enum(['iOS', 'watchOS', 'tvOS', 'visionOS']) - .optional() - .describe('Target platform (defaults to iOS)'), + platform: z.enum(['iOS', 'watchOS', 'tvOS', 'visionOS']).optional().describe('default: iOS'), }; const baseSchemaObject = z.object({ @@ -157,7 +154,7 @@ export async function get_device_app_pathLogic( export default { name: 'get_device_app_path', - description: 'Retrieves the built app path for a connected device.', + description: 'Get device built app path.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/device/install_app_device.ts b/src/mcp/tools/device/install_app_device.ts index 9a6d3917..b3cca6f5 100644 --- a/src/mcp/tools/device/install_app_device.ts +++ b/src/mcp/tools/device/install_app_device.ts @@ -21,9 +21,7 @@ const installAppDeviceSchema = z.object({ .string() .min(1, { message: 'Device ID cannot be empty' }) .describe('UDID of the device (obtained from list_devices)'), - appPath: z - .string() - .describe('Path to the .app bundle to install (full path to the .app directory)'), + appPath: z.string(), }); const publicSchemaObject = installAppDeviceSchema.omit({ deviceId: true } as const); @@ -87,7 +85,7 @@ export async function install_app_deviceLogic( export default { name: 'install_app_device', - description: 'Installs an app on a connected device.', + description: 'Install app on device.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: installAppDeviceSchema, diff --git a/src/mcp/tools/device/launch_app_device.ts b/src/mcp/tools/device/launch_app_device.ts index df55fc6c..f560ae17 100644 --- a/src/mcp/tools/device/launch_app_device.ts +++ b/src/mcp/tools/device/launch_app_device.ts @@ -31,9 +31,7 @@ type LaunchDataResponse = { // Define schema as ZodObject const launchAppDeviceSchema = z.object({ deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), - bundleId: z - .string() - .describe('Bundle identifier of the app to launch (e.g., "com.example.MyApp")'), + bundleId: z.string(), }); const publicSchemaObject = launchAppDeviceSchema.omit({ deviceId: true } as const); @@ -148,7 +146,7 @@ export async function launch_app_deviceLogic( export default { name: 'launch_app_device', - description: 'Launches an app on a connected device.', + description: 'Launch app on device.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: launchAppDeviceSchema, diff --git a/src/mcp/tools/device/list_devices.ts b/src/mcp/tools/device/list_devices.ts index 8ca67d6d..9efd6855 100644 --- a/src/mcp/tools/device/list_devices.ts +++ b/src/mcp/tools/device/list_devices.ts @@ -430,8 +430,7 @@ export async function list_devicesLogic( export default { name: 'list_devices', - description: - 'Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.', + description: 'List connected devices.', schema: listDevicesSchema.shape, // MCP SDK compatibility annotations: { title: 'List Devices', diff --git a/src/mcp/tools/device/stop_app_device.ts b/src/mcp/tools/device/stop_app_device.ts index 7a719e55..2cbea4f1 100644 --- a/src/mcp/tools/device/stop_app_device.ts +++ b/src/mcp/tools/device/stop_app_device.ts @@ -18,7 +18,7 @@ import { // Define schema as ZodObject const stopAppDeviceSchema = z.object({ deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), - processId: z.number().describe('Process ID (PID) of the app to stop'), + processId: z.number(), }); // Use z.infer for type safety @@ -89,7 +89,7 @@ export async function stop_app_deviceLogic( export default { name: 'stop_app_device', - description: 'Stops a running app on a connected device.', + description: 'Stop device app.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: stopAppDeviceSchema, diff --git a/src/mcp/tools/device/test_device.ts b/src/mcp/tools/device/test_device.ts index 1e78e62c..005d40a8 100644 --- a/src/mcp/tools/device/test_device.ts +++ b/src/mcp/tools/device/test_device.ts @@ -34,13 +34,10 @@ const baseSchemaObject = z.object({ scheme: z.string().describe('The scheme to test'), deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), configuration: z.string().optional().describe('Build configuration (Debug, Release)'), - derivedDataPath: z.string().optional().describe('Path to derived data directory'), - extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'), - preferXcodebuild: z.boolean().optional().describe('Prefer xcodebuild over faster alternatives'), - platform: z - .enum(['iOS', 'watchOS', 'tvOS', 'visionOS']) - .optional() - .describe('Target platform (defaults to iOS)'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), + preferXcodebuild: z.boolean().optional(), + platform: z.enum(['iOS', 'watchOS', 'tvOS', 'visionOS']).optional(), testRunnerEnv: z .record(z.string(), z.string()) .optional() @@ -287,7 +284,7 @@ export async function testDeviceLogic( export default { name: 'test_device', - description: 'Runs tests on a physical Apple device.', + description: 'Test on device.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/doctor/doctor.ts b/src/mcp/tools/doctor/doctor.ts index 52ea979b..74d080a3 100644 --- a/src/mcp/tools/doctor/doctor.ts +++ b/src/mcp/tools/doctor/doctor.ts @@ -18,7 +18,7 @@ const LOG_PREFIX = '[Doctor]'; // Define schema as ZodObject const doctorSchema = z.object({ - enabled: z.boolean().optional().describe('Optional: dummy parameter to satisfy MCP protocol'), + enabled: z.boolean().optional(), }); // Use z.infer for type safety @@ -293,8 +293,7 @@ async function doctorMcpHandler( export default { name: 'doctor', - description: - 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', + description: 'MCP environment info.', schema: doctorSchema.shape, // MCP SDK compatibility annotations: { title: 'Doctor', 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 7f5ccb78..959cdfeb 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 @@ -64,7 +64,7 @@ describe('start_device_log_cap plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe('Starts log capture on a connected device.'); + expect(plugin.description).toBe('Start device log capture.'); }); it('should have correct schema structure', () => { 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 3feff08c..95e2c90a 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 @@ -22,9 +22,7 @@ describe('start_sim_log_cap plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe( - "Starts capturing logs from a specified simulator. Returns a session ID. Use subsystemFilter to control what logs are captured: 'app' (default), 'all' (everything), 'swiftui' (includes Self._printChanges()), or custom subsystems.", - ); + expect(plugin.description).toBe('Start sim log capture.'); }); it('should have handler as a function', () => { diff --git a/src/mcp/tools/logging/start_device_log_cap.ts b/src/mcp/tools/logging/start_device_log_cap.ts index 8ebda090..8c7c1f37 100644 --- a/src/mcp/tools/logging/start_device_log_cap.ts +++ b/src/mcp/tools/logging/start_device_log_cap.ts @@ -614,7 +614,7 @@ async function cleanOldDeviceLogs(fileSystemExecutor: FileSystemExecutor): Promi // Define schema as ZodObject const startDeviceLogCapSchema = z.object({ deviceId: z.string().describe('UDID of the device (obtained from list_devices)'), - bundleId: z.string().describe('Bundle identifier of the app to launch and capture logs for.'), + bundleId: z.string(), }); const publicSchemaObject = startDeviceLogCapSchema.omit({ deviceId: true } as const); @@ -667,7 +667,7 @@ export async function start_device_log_capLogic( export default { name: 'start_device_log_cap', - description: 'Starts log capture on a connected device.', + description: 'Start device log capture.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: startDeviceLogCapSchema, diff --git a/src/mcp/tools/logging/start_sim_log_cap.ts b/src/mcp/tools/logging/start_sim_log_cap.ts index 0ae9628d..569d9fd2 100644 --- a/src/mcp/tools/logging/start_sim_log_cap.ts +++ b/src/mcp/tools/logging/start_sim_log_cap.ts @@ -19,17 +19,12 @@ const startSimLogCapSchema = z.object({ simulatorId: z .uuid() .describe('UUID of the simulator to capture logs from (obtained from list_simulators).'), - bundleId: z.string().describe('Bundle identifier of the app to capture logs for.'), - captureConsole: z - .boolean() - .optional() - .describe('Whether to capture console output (requires app relaunch).'), + bundleId: z.string(), + captureConsole: z.boolean().optional(), subsystemFilter: z .union([z.enum(['app', 'all', 'swiftui']), z.array(z.string()).min(1)]) .default('app') - .describe( - "Controls which log subsystems to capture. Options: 'app' (default, only app logs), 'all' (capture all system logs), 'swiftui' (app + SwiftUI logs for Self._printChanges()), or an array of custom subsystem strings.", - ), + .describe('app|all|swiftui|[subsystem]'), }); // Use z.infer for type safety @@ -90,8 +85,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'start_sim_log_cap', - description: - "Starts capturing logs from a specified simulator. Returns a session ID. Use subsystemFilter to control what logs are captured: 'app' (default), 'all' (everything), 'swiftui' (includes Self._printChanges()), or custom subsystems.", + description: 'Start sim log capture.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: startSimLogCapSchema, diff --git a/src/mcp/tools/logging/stop_device_log_cap.ts b/src/mcp/tools/logging/stop_device_log_cap.ts index 575c32e9..efc1d59e 100644 --- a/src/mcp/tools/logging/stop_device_log_cap.ts +++ b/src/mcp/tools/logging/stop_device_log_cap.ts @@ -18,7 +18,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const stopDeviceLogCapSchema = z.object({ - logSessionId: z.string().describe('The session ID returned by start_device_log_cap.'), + logSessionId: z.string(), }); // Use z.infer for type safety @@ -328,7 +328,7 @@ export async function stopDeviceLogCapture( export default { name: 'stop_device_log_cap', - description: 'Stops an active Apple device log capture session and returns the captured logs.', + description: 'Stop device log capture.', schema: stopDeviceLogCapSchema.shape, // MCP SDK compatibility annotations: { title: 'Stop Device Log Capture', diff --git a/src/mcp/tools/logging/stop_sim_log_cap.ts b/src/mcp/tools/logging/stop_sim_log_cap.ts index a143ba80..6aa8614c 100644 --- a/src/mcp/tools/logging/stop_sim_log_cap.ts +++ b/src/mcp/tools/logging/stop_sim_log_cap.ts @@ -14,7 +14,7 @@ import type { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts'; // Define schema as ZodObject const stopSimLogCapSchema = z.object({ - logSessionId: z.string().describe('The session ID returned by start_sim_log_cap.'), + logSessionId: z.string(), }); // Use z.infer for type safety @@ -54,7 +54,7 @@ export async function stop_sim_log_capLogic( export default { name: 'stop_sim_log_cap', - description: 'Stops an active simulator log capture session and returns the captured logs.', + description: 'Stop sim log capture.', schema: stopSimLogCapSchema.shape, // MCP SDK compatibility annotations: { title: 'Stop Simulator Log Capture', diff --git a/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts b/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts index 1b992a54..9d0217ad 100644 --- a/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts +++ b/src/mcp/tools/macos/__tests__/get_mac_app_path.test.ts @@ -24,7 +24,7 @@ describe('get_mac_app_path plugin', () => { }); it('should have correct description', () => { - expect(getMacAppPath.description).toBe('Retrieves the built macOS app bundle path.'); + expect(getMacAppPath.description).toBe('Get macOS built app path.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/macos/__tests__/test_macos.test.ts b/src/mcp/tools/macos/__tests__/test_macos.test.ts index 1758e50b..ac305835 100644 --- a/src/mcp/tools/macos/__tests__/test_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/test_macos.test.ts @@ -34,7 +34,7 @@ describe('test_macos plugin (unified)', () => { }); it('should have correct description', () => { - expect(testMacos.description).toBe('Runs tests for a macOS target.'); + expect(testMacos.description).toBe('Test macOS target.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/macos/build_macos.ts b/src/mcp/tools/macos/build_macos.ts index 66bdab09..810385b8 100644 --- a/src/mcp/tools/macos/build_macos.ts +++ b/src/mcp/tools/macos/build_macos.ts @@ -33,19 +33,13 @@ const baseSchemaObject = z.object({ workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), scheme: z.string().describe('The scheme to use'), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Path where build products and other derived data will go'), + derivedDataPath: z.string().optional(), arch: z .enum(['arm64', 'x86_64']) .optional() .describe('Architecture to build for (arm64 or x86_64). For macOS only.'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), - preferXcodebuild: z - .boolean() - .optional() - .describe('If true, prefers xcodebuild over the experimental incremental build system'), + extraArgs: z.array(z.string()).optional(), + preferXcodebuild: z.boolean().optional(), }); const publicSchemaObject = baseSchemaObject.omit({ @@ -101,7 +95,7 @@ export async function buildMacOSLogic( export default { name: 'build_macos', - description: 'Builds a macOS app.', + description: 'Build macOS app.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/macos/build_run_macos.ts b/src/mcp/tools/macos/build_run_macos.ts index 755629eb..28a806a7 100644 --- a/src/mcp/tools/macos/build_run_macos.ts +++ b/src/mcp/tools/macos/build_run_macos.ts @@ -24,19 +24,13 @@ const baseSchemaObject = z.object({ workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), scheme: z.string().describe('The scheme to use'), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Path where build products and other derived data will go'), + derivedDataPath: z.string().optional(), arch: z .enum(['arm64', 'x86_64']) .optional() .describe('Architecture to build for (arm64 or x86_64). For macOS only.'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), - preferXcodebuild: z - .boolean() - .optional() - .describe('If true, prefers xcodebuild over the experimental incremental build system'), + extraArgs: z.array(z.string()).optional(), + preferXcodebuild: z.boolean().optional(), }); const publicSchemaObject = baseSchemaObject.omit({ @@ -219,7 +213,7 @@ export async function buildRunMacOSLogic( export default { name: 'build_run_macos', - description: 'Builds and runs a macOS app.', + description: 'Build and run macOS app.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/macos/get_mac_app_path.ts b/src/mcp/tools/macos/get_mac_app_path.ts index 5339de05..e8c21422 100644 --- a/src/mcp/tools/macos/get_mac_app_path.ts +++ b/src/mcp/tools/macos/get_mac_app_path.ts @@ -20,8 +20,8 @@ import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; const baseOptions = { scheme: z.string().describe('The scheme to use'), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z.string().optional().describe('Path to derived data directory'), - extraArgs: z.array(z.string()).optional().describe('Additional arguments to pass to xcodebuild'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), arch: z .enum(['arm64', 'x86_64']) .optional() @@ -191,7 +191,7 @@ export async function get_mac_app_pathLogic( export default { name: 'get_mac_app_path', - description: 'Retrieves the built macOS app bundle path.', + description: 'Get macOS built app path.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/macos/launch_mac_app.ts b/src/mcp/tools/macos/launch_mac_app.ts index c252a55b..200c51f3 100644 --- a/src/mcp/tools/macos/launch_mac_app.ts +++ b/src/mcp/tools/macos/launch_mac_app.ts @@ -15,10 +15,8 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const launchMacAppSchema = z.object({ - appPath: z - .string() - .describe('Path to the macOS .app bundle to launch (full path to the .app directory)'), - args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), + appPath: z.string(), + args: z.array(z.string()).optional(), }); // Use z.infer for type safety @@ -76,8 +74,7 @@ export async function launch_mac_appLogic( export default { name: 'launch_mac_app', - description: - "Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.", + description: 'Launch macOS app.', schema: launchMacAppSchema.shape, // MCP SDK compatibility annotations: { title: 'Launch macOS App', diff --git a/src/mcp/tools/macos/stop_mac_app.ts b/src/mcp/tools/macos/stop_mac_app.ts index b9fec289..20b7484d 100644 --- a/src/mcp/tools/macos/stop_mac_app.ts +++ b/src/mcp/tools/macos/stop_mac_app.ts @@ -7,11 +7,8 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const stopMacAppSchema = z.object({ - appName: z - .string() - .optional() - .describe('Name of the application to stop (e.g., "Calculator" or "MyApp")'), - processId: z.number().optional().describe('Process ID (PID) of the application to stop'), + appName: z.string().optional(), + processId: z.number().optional(), }); // Use z.infer for type safety @@ -80,7 +77,7 @@ export async function stop_mac_appLogic( export default { name: 'stop_mac_app', - description: 'Stops a running macOS application. Can stop by app name or process ID.', + description: 'Stop macOS app.', schema: stopMacAppSchema.shape, // MCP SDK compatibility annotations: { title: 'Stop macOS App', diff --git a/src/mcp/tools/macos/test_macos.ts b/src/mcp/tools/macos/test_macos.ts index 238aacea..2b02b1b5 100644 --- a/src/mcp/tools/macos/test_macos.ts +++ b/src/mcp/tools/macos/test_macos.ts @@ -33,15 +33,9 @@ const baseSchemaObject = z.object({ workspacePath: z.string().optional().describe('Path to the .xcworkspace file'), scheme: z.string().describe('The scheme to use'), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Path where build products and other derived data will go'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), - preferXcodebuild: z - .boolean() - .optional() - .describe('If true, prefers xcodebuild over the experimental incremental build system'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), + preferXcodebuild: z.boolean().optional(), testRunnerEnv: z .record(z.string(), z.string()) .optional() @@ -329,7 +323,7 @@ export async function testMacosLogic( export default { name: 'test_macos', - description: 'Runs tests for a macOS target.', + description: 'Test macOS target.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, 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 21fa55b2..177e3fbe 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 @@ -37,9 +37,7 @@ describe('get_app_bundle_id plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe( - "Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS). IMPORTANT: You MUST provide the appPath parameter. Example: get_app_bundle_id({ appPath: '/path/to/your/app.app' })", - ); + expect(plugin.description).toBe('Extract bundle id from .app.'); }); it('should have handler function', () => { 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 65f1bfd6..7d7a15af 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 @@ -27,9 +27,7 @@ describe('get_mac_bundle_id plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe( - "Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.", - ); + expect(plugin.description).toBe('Extract bundle id from macOS .app.'); }); it('should have handler function', () => { 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 0e4a55ac..4a987e51 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 @@ -14,7 +14,7 @@ describe('show_build_settings plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe('Shows xcodebuild build settings.'); + expect(plugin.description).toBe('Show build settings.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/project-discovery/discover_projs.ts b/src/mcp/tools/project-discovery/discover_projs.ts index 5959844f..3fbdc904 100644 --- a/src/mcp/tools/project-discovery/discover_projs.ts +++ b/src/mcp/tools/project-discovery/discover_projs.ts @@ -134,17 +134,9 @@ async function _findProjectsRecursive( // Define schema as ZodObject const discoverProjsSchema = z.object({ - workspaceRoot: z.string().describe('The absolute path of the workspace root to scan within.'), - scanPath: z - .string() - .optional() - .describe('Optional: Path relative to workspace root to scan. Defaults to workspace root.'), - maxDepth: z - .number() - .int() - .nonnegative() - .optional() - .describe(`Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}.`), + workspaceRoot: z.string(), + scanPath: z.string().optional(), + maxDepth: z.number().int().nonnegative().optional(), }); // Use z.infer for type safety diff --git a/src/mcp/tools/project-discovery/get_app_bundle_id.ts b/src/mcp/tools/project-discovery/get_app_bundle_id.ts index bd22ee2e..48fc745b 100644 --- a/src/mcp/tools/project-discovery/get_app_bundle_id.ts +++ b/src/mcp/tools/project-discovery/get_app_bundle_id.ts @@ -18,11 +18,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const getAppBundleIdSchema = z.object({ - appPath: z - .string() - .describe( - 'Path to the .app bundle to extract bundle ID from (full path to the .app directory)', - ), + appPath: z.string().describe('Path to the .app bundle'), }); // Use z.infer for type safety @@ -125,8 +121,7 @@ export async function get_app_bundle_idLogic( export default { name: 'get_app_bundle_id', - description: - "Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS). IMPORTANT: You MUST provide the appPath parameter. Example: get_app_bundle_id({ appPath: '/path/to/your/app.app' })", + description: 'Extract bundle id from .app.', schema: getAppBundleIdSchema.shape, // MCP SDK compatibility annotations: { title: 'Get App Bundle ID', diff --git a/src/mcp/tools/project-discovery/get_mac_bundle_id.ts b/src/mcp/tools/project-discovery/get_mac_bundle_id.ts index 57c1de6b..b7b99941 100644 --- a/src/mcp/tools/project-discovery/get_mac_bundle_id.ts +++ b/src/mcp/tools/project-discovery/get_mac_bundle_id.ts @@ -28,11 +28,7 @@ async function executeSyncCommand(command: string, executor: CommandExecutor): P // Define schema as ZodObject const getMacBundleIdSchema = z.object({ - appPath: z - .string() - .describe( - 'Path to the macOS .app bundle to extract bundle ID from (full path to the .app directory)', - ), + appPath: z.string().describe('Path to the .app bundle'), }); // Use z.infer for type safety @@ -122,8 +118,7 @@ export async function get_mac_bundle_idLogic( export default { name: 'get_mac_bundle_id', - description: - "Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.", + description: 'Extract bundle id from macOS .app.', schema: getMacBundleIdSchema.shape, // MCP SDK compatibility annotations: { title: 'Get Mac Bundle ID', diff --git a/src/mcp/tools/project-discovery/list_schemes.ts b/src/mcp/tools/project-discovery/list_schemes.ts index b14e925b..a49a9c60 100644 --- a/src/mcp/tools/project-discovery/list_schemes.ts +++ b/src/mcp/tools/project-discovery/list_schemes.ts @@ -123,7 +123,7 @@ const publicSchemaObject = baseSchemaObject.omit({ export default { name: 'list_schemes', - description: 'Lists schemes for a project or workspace.', + description: 'List Xcode schemes.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/project-discovery/show_build_settings.ts b/src/mcp/tools/project-discovery/show_build_settings.ts index 46de6c0d..aab3b737 100644 --- a/src/mcp/tools/project-discovery/show_build_settings.ts +++ b/src/mcp/tools/project-discovery/show_build_settings.ts @@ -114,7 +114,7 @@ const publicSchemaObject = baseSchemaObject.omit({ export default { name: 'show_build_settings', - description: 'Shows xcodebuild build settings.', + description: 'Show build settings.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, 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 28866c9a..afabef34 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 @@ -72,9 +72,7 @@ describe('scaffold_ios_project plugin', () => { }); it('should have correct description field', () => { - expect(scaffoldIosProject.description).toBe( - 'Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.', - ); + expect(scaffoldIosProject.description).toBe('Scaffold iOS project.'); }); it('should have handler as function', () => { diff --git a/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts b/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts index c98a43fe..46a6a287 100644 --- a/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts +++ b/src/mcp/tools/project-scaffolding/scaffold_ios_project.ts @@ -18,52 +18,25 @@ import { ToolResponse } from '../../../types/common.ts'; // Common base schema for both iOS and macOS const BaseScaffoldSchema = z.object({ - projectName: z.string().min(1).describe('Name of the new project'), - outputPath: z.string().describe('Path where the project should be created'), - bundleIdentifier: z - .string() - .optional() - .describe( - 'Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname', - ), - displayName: z - .string() - .optional() - .describe( - 'App display name (shown on home screen/dock). If not provided, will use projectName', - ), - marketingVersion: z - .string() - .optional() - .describe('Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0'), - currentProjectVersion: z - .string() - .optional() - .describe('Build number (e.g., 1, 42, 100). If not provided, will use 1'), - customizeNames: z - .boolean() - .default(true) - .describe('Whether to customize project names and identifiers. Default is true.'), + projectName: z.string().min(1), + outputPath: z.string(), + bundleIdentifier: z.string().optional(), + displayName: z.string().optional(), + marketingVersion: z.string().optional(), + currentProjectVersion: z.string().optional(), + customizeNames: z.boolean().default(true), }); // iOS-specific schema const ScaffoldiOSProjectSchema = BaseScaffoldSchema.extend({ - deploymentTarget: z - .string() - .optional() - .describe('iOS deployment target (e.g., 18.4, 17.0). If not provided, will use 18.4'), - targetedDeviceFamily: z - .array(z.enum(['iphone', 'ipad', 'universal'])) - .optional() - .describe('Targeted device families'), + deploymentTarget: z.string().optional(), + targetedDeviceFamily: z.array(z.enum(['iphone', 'ipad', 'universal'])).optional(), supportedOrientations: z .array(z.enum(['portrait', 'landscape-left', 'landscape-right', 'portrait-upside-down'])) - .optional() - .describe('Supported orientations for iPhone'), + .optional(), supportedOrientationsIpad: z .array(z.enum(['portrait', 'landscape-left', 'landscape-right', 'portrait-upside-down'])) - .optional() - .describe('Supported orientations for iPad'), + .optional(), }); /** @@ -498,8 +471,7 @@ async function scaffoldProject( export default { name: 'scaffold_ios_project', - description: - 'Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.', + description: 'Scaffold iOS project.', schema: ScaffoldiOSProjectSchema.shape, annotations: { title: 'Scaffold iOS Project', diff --git a/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts b/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts index cc7841bd..35df0491 100644 --- a/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts +++ b/src/mcp/tools/project-scaffolding/scaffold_macos_project.ts @@ -19,40 +19,18 @@ import { FileSystemExecutor } from '../../../utils/FileSystemExecutor.ts'; // Common base schema for both iOS and macOS const BaseScaffoldSchema = z.object({ - projectName: z.string().min(1).describe('Name of the new project'), - outputPath: z.string().describe('Path where the project should be created'), - bundleIdentifier: z - .string() - .optional() - .describe( - 'Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname', - ), - displayName: z - .string() - .optional() - .describe( - 'App display name (shown on home screen/dock). If not provided, will use projectName', - ), - marketingVersion: z - .string() - .optional() - .describe('Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0'), - currentProjectVersion: z - .string() - .optional() - .describe('Build number (e.g., 1, 42, 100). If not provided, will use 1'), - customizeNames: z - .boolean() - .default(true) - .describe('Whether to customize project names and identifiers. Default is true.'), + projectName: z.string().min(1), + outputPath: z.string(), + bundleIdentifier: z.string().optional(), + displayName: z.string().optional(), + marketingVersion: z.string().optional(), + currentProjectVersion: z.string().optional(), + customizeNames: z.boolean().default(true), }); // macOS-specific schema const ScaffoldmacOSProjectSchema = BaseScaffoldSchema.extend({ - deploymentTarget: z - .string() - .optional() - .describe('macOS deployment target (e.g., 15.4, 14.0). If not provided, will use 15.4'), + deploymentTarget: z.string().optional(), }); // Use z.infer for type safety @@ -404,8 +382,7 @@ export async function scaffold_macos_projectLogic( export default { name: 'scaffold_macos_project', - description: - 'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.', + description: 'Scaffold macOS project.', schema: ScaffoldmacOSProjectSchema.shape, annotations: { title: 'Scaffold macOS Project', diff --git a/src/mcp/tools/session-management/session_clear_defaults.ts b/src/mcp/tools/session-management/session_clear_defaults.ts index d6a58983..94de3bb8 100644 --- a/src/mcp/tools/session-management/session_clear_defaults.ts +++ b/src/mcp/tools/session-management/session_clear_defaults.ts @@ -31,7 +31,7 @@ export async function sessionClearDefaultsLogic(params: Params): Promise { }); it('should have correct description', () => { - expect(eraseSims.description).toBe('Erases a simulator by UDID.'); + expect(eraseSims.description).toBe('Erase simulator.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts b/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts index 20107195..8cc0be06 100644 --- a/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/reset_sim_location.test.ts @@ -10,9 +10,7 @@ describe('reset_sim_location plugin', () => { }); it('should have correct description field', () => { - expect(resetSimLocationPlugin.description).toBe( - "Resets the simulator's location to default.", - ); + expect(resetSimLocationPlugin.description).toBe('Reset sim location.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts b/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts index d102730d..4d5b1b69 100644 --- a/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/set_sim_appearance.test.ts @@ -13,9 +13,7 @@ describe('set_sim_appearance plugin', () => { }); it('should have correct description field', () => { - expect(setSimAppearancePlugin.description).toBe( - 'Sets the appearance mode (dark/light) of an iOS simulator.', - ); + expect(setSimAppearancePlugin.description).toBe('Set sim appearance.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts b/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts index 14953edd..acbc9c76 100644 --- a/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/set_sim_location.test.ts @@ -22,7 +22,7 @@ describe('set_sim_location tool', () => { }); it('should have correct description', () => { - expect(setSimLocation.description).toBe('Sets a custom GPS location for the simulator.'); + expect(setSimLocation.description).toBe('Set sim location.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts b/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts index 55bc073d..edc4b963 100644 --- a/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts +++ b/src/mcp/tools/simulator-management/__tests__/sim_statusbar.test.ts @@ -20,9 +20,7 @@ describe('sim_statusbar tool', () => { }); it('should have correct description', () => { - expect(simStatusbar.description).toBe( - 'Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).', - ); + expect(simStatusbar.description).toBe('Set sim status bar network.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator-management/erase_sims.ts b/src/mcp/tools/simulator-management/erase_sims.ts index 3ee50c05..91fc53b5 100644 --- a/src/mcp/tools/simulator-management/erase_sims.ts +++ b/src/mcp/tools/simulator-management/erase_sims.ts @@ -10,10 +10,7 @@ import { const eraseSimsBaseSchema = z .object({ simulatorId: z.uuid().describe('UDID of the simulator to erase.'), - shutdownFirst: z - .boolean() - .optional() - .describe('If true, shuts down the simulator before erasing.'), + shutdownFirst: z.boolean().optional(), }) .passthrough(); @@ -85,7 +82,7 @@ const publicSchemaObject = eraseSimsSchema.omit({ simulatorId: true } as const). export default { name: 'erase_sims', - description: 'Erases a simulator by UDID.', + description: 'Erase simulator.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: eraseSimsSchema, diff --git a/src/mcp/tools/simulator-management/reset_sim_location.ts b/src/mcp/tools/simulator-management/reset_sim_location.ts index f9aef57b..9de8bb28 100644 --- a/src/mcp/tools/simulator-management/reset_sim_location.ts +++ b/src/mcp/tools/simulator-management/reset_sim_location.ts @@ -91,7 +91,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'reset_sim_location', - description: "Resets the simulator's location to default.", + description: 'Reset sim location.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: resetSimulatorLocationSchema, diff --git a/src/mcp/tools/simulator-management/set_sim_appearance.ts b/src/mcp/tools/simulator-management/set_sim_appearance.ts index 94dec95d..99da8087 100644 --- a/src/mcp/tools/simulator-management/set_sim_appearance.ts +++ b/src/mcp/tools/simulator-management/set_sim_appearance.ts @@ -10,7 +10,7 @@ import { // Define schema as ZodObject const setSimAppearanceSchema = z.object({ simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'), - mode: z.enum(['dark', 'light']).describe('The appearance mode to set (either "dark" or "light")'), + mode: z.enum(['dark', 'light']).describe('dark|light'), }); // Use z.infer for type safety @@ -93,7 +93,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'set_sim_appearance', - description: 'Sets the appearance mode (dark/light) of an iOS simulator.', + description: 'Set sim appearance.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: setSimAppearanceSchema, diff --git a/src/mcp/tools/simulator-management/set_sim_location.ts b/src/mcp/tools/simulator-management/set_sim_location.ts index d4208cda..e5edbee8 100644 --- a/src/mcp/tools/simulator-management/set_sim_location.ts +++ b/src/mcp/tools/simulator-management/set_sim_location.ts @@ -10,8 +10,8 @@ import { // Define schema as ZodObject const setSimulatorLocationSchema = z.object({ simulatorId: z.uuid().describe('UUID of the simulator to use (obtained from list_simulators)'), - latitude: z.number().describe('The latitude for the custom location.'), - longitude: z.number().describe('The longitude for the custom location.'), + latitude: z.number(), + longitude: z.number(), }); // Use z.infer for type safety @@ -121,7 +121,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'set_sim_location', - description: 'Sets a custom GPS location for the simulator.', + description: 'Set sim location.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: setSimulatorLocationSchema, diff --git a/src/mcp/tools/simulator-management/sim_statusbar.ts b/src/mcp/tools/simulator-management/sim_statusbar.ts index 1dca37be..50ee79e0 100644 --- a/src/mcp/tools/simulator-management/sim_statusbar.ts +++ b/src/mcp/tools/simulator-management/sim_statusbar.ts @@ -25,9 +25,7 @@ const simStatusbarSchema = z.object({ '5g-uwb', '5g-uc', ]) - .describe( - 'Data network type to display in status bar. Use "clear" to reset all overrides. Valid values: clear, hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc.', - ), + .describe('clear|hide|wifi|3g|4g|lte|lte-a|lte+|5g|5g+|5g-uwb|5g-uc'), }); // Use z.infer for type safety @@ -94,8 +92,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'sim_statusbar', - description: - 'Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).', + description: 'Set sim status bar network.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: simStatusbarSchema, 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 57e9bbc7..d0d51f53 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -24,7 +24,7 @@ describe('build_run_sim tool', () => { }); it('should have correct description', () => { - expect(buildRunSim.description).toBe('Builds and runs an app on an iOS simulator.'); + expect(buildRunSim.description).toBe('Build and run iOS sim.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator/__tests__/build_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_sim.test.ts index 5418ac0b..0ee980a1 100644 --- a/src/mcp/tools/simulator/__tests__/build_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_sim.test.ts @@ -21,7 +21,7 @@ describe('build_sim tool', () => { }); it('should have correct description', () => { - expect(buildSim.description).toBe('Builds an app for an iOS simulator.'); + expect(buildSim.description).toBe('Build for iOS sim.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator/__tests__/get_sim_app_path.test.ts b/src/mcp/tools/simulator/__tests__/get_sim_app_path.test.ts index 0611ed21..c9075bb0 100644 --- a/src/mcp/tools/simulator/__tests__/get_sim_app_path.test.ts +++ b/src/mcp/tools/simulator/__tests__/get_sim_app_path.test.ts @@ -22,7 +22,7 @@ describe('get_sim_app_path tool', () => { }); it('should have concise description', () => { - expect(getSimAppPath.description).toBe('Retrieves the built app path for an iOS simulator.'); + expect(getSimAppPath.description).toBe('Get sim built app path.'); }); it('should have handler function', () => { 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 bd5ca9b7..86495e39 100644 --- a/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/install_app_sim.test.ts @@ -21,7 +21,7 @@ describe('install_app_sim tool', () => { }); it('should have concise description', () => { - expect(installAppSim.description).toBe('Installs an app in an iOS simulator.'); + expect(installAppSim.description).toBe('Install app on sim.'); }); it('should expose public schema with only appPath', () => { 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 28d0983f..d47998c6 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 @@ -20,9 +20,7 @@ describe('launch_app_logs_sim tool', () => { describe('Export Field Validation (Literal)', () => { it('should expose correct metadata', () => { expect(launchAppLogsSim.name).toBe('launch_app_logs_sim'); - expect(launchAppLogsSim.description).toBe( - 'Launches an app in an iOS simulator and captures its logs.', - ); + expect(launchAppLogsSim.description).toBe('Launch sim app with logs.'); }); it('should expose only non-session fields in public schema', () => { 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 45bf74e1..bfdb3fda 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts @@ -12,7 +12,7 @@ describe('launch_app_sim tool', () => { describe('Export Field Validation (Literal)', () => { it('should expose correct name and description', () => { expect(launchAppSim.name).toBe('launch_app_sim'); - expect(launchAppSim.description).toBe('Launches an app in an iOS simulator.'); + expect(launchAppSim.description).toBe('Launch app on simulator.'); }); it('should expose only non-session fields in public schema', () => { diff --git a/src/mcp/tools/simulator/__tests__/list_sims.test.ts b/src/mcp/tools/simulator/__tests__/list_sims.test.ts index 04c61512..16ca2993 100644 --- a/src/mcp/tools/simulator/__tests__/list_sims.test.ts +++ b/src/mcp/tools/simulator/__tests__/list_sims.test.ts @@ -24,7 +24,7 @@ describe('list_sims tool', () => { }); it('should have correct description', () => { - expect(listSims.description).toBe('Lists available iOS simulators with their UUIDs. '); + expect(listSims.description).toBe('List iOS simulators.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator/__tests__/open_sim.test.ts b/src/mcp/tools/simulator/__tests__/open_sim.test.ts index 22e25df5..808c344a 100644 --- a/src/mcp/tools/simulator/__tests__/open_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/open_sim.test.ts @@ -20,7 +20,7 @@ describe('open_sim tool', () => { }); it('should have correct description field', () => { - expect(openSim.description).toBe('Opens the iOS Simulator app.'); + expect(openSim.description).toBe('Open Simulator app.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator/__tests__/screenshot.test.ts b/src/mcp/tools/simulator/__tests__/screenshot.test.ts index 5562f38d..be01bc7c 100644 --- a/src/mcp/tools/simulator/__tests__/screenshot.test.ts +++ b/src/mcp/tools/simulator/__tests__/screenshot.test.ts @@ -27,9 +27,7 @@ describe('screenshot plugin', () => { }); it('should have correct description field', () => { - expect(screenshotPlugin.description).toBe( - "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", - ); + expect(screenshotPlugin.description).toBe('Capture screenshot.'); }); it('should have handler function', () => { 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 6b174976..4c0cb9cc 100644 --- a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts @@ -16,7 +16,7 @@ describe('stop_app_sim tool', () => { describe('Export Field Validation (Literal)', () => { it('should expose correct metadata', () => { expect(plugin.name).toBe('stop_app_sim'); - expect(plugin.description).toBe('Stops an app running in an iOS simulator.'); + expect(plugin.description).toBe('Stop sim app.'); }); it('should expose public schema with only bundleId', () => { diff --git a/src/mcp/tools/simulator/__tests__/test_sim.test.ts b/src/mcp/tools/simulator/__tests__/test_sim.test.ts index 69bf3ab8..69be7100 100644 --- a/src/mcp/tools/simulator/__tests__/test_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/test_sim.test.ts @@ -19,7 +19,7 @@ describe('test_sim tool', () => { }); it('should have concise description', () => { - expect(testSim.description).toBe('Runs tests on an iOS simulator.'); + expect(testSim.description).toBe('Test on iOS sim.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator/boot_sim.ts b/src/mcp/tools/simulator/boot_sim.ts index 95c112c1..5235de67 100644 --- a/src/mcp/tools/simulator/boot_sim.ts +++ b/src/mcp/tools/simulator/boot_sim.ts @@ -70,7 +70,7 @@ Next steps: export default { name: 'boot_sim', - description: 'Boots an iOS simulator.', + description: 'Boot iOS simulator.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: bootSimSchemaObject, diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index 84dba281..bac516fb 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -36,21 +36,13 @@ const baseOptions = { "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both", ), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Path where build products and other derived data will go'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), useLatestOS: z .boolean() .optional() .describe('Whether to use the latest OS version for the named simulator'), - preferXcodebuild: z - .boolean() - .optional() - .describe( - 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', - ), + preferXcodebuild: z.boolean().optional(), }; const baseSchemaObject = z.object({ @@ -506,7 +498,7 @@ const publicSchemaObject = baseSchemaObject.omit({ export default { name: 'build_run_sim', - description: 'Builds and runs an app on an iOS simulator.', + description: 'Build and run iOS sim.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/simulator/build_sim.ts b/src/mcp/tools/simulator/build_sim.ts index 2280b3ec..d707ced2 100644 --- a/src/mcp/tools/simulator/build_sim.ts +++ b/src/mcp/tools/simulator/build_sim.ts @@ -34,21 +34,13 @@ const baseOptions = { "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both", ), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Path where build products and other derived data will go'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), useLatestOS: z .boolean() .optional() .describe('Whether to use the latest OS version for the named simulator'), - preferXcodebuild: z - .boolean() - .optional() - .describe( - 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', - ), + preferXcodebuild: z.boolean().optional(), }; const baseSchemaObject = z.object({ @@ -153,7 +145,7 @@ const publicSchemaObject = baseSchemaObject.omit({ export default { name: 'build_sim', - description: 'Builds an app for an iOS simulator.', + description: 'Build for iOS sim.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/simulator/get_sim_app_path.ts b/src/mcp/tools/simulator/get_sim_app_path.ts index e111feed..56b52350 100644 --- a/src/mcp/tools/simulator/get_sim_app_path.ts +++ b/src/mcp/tools/simulator/get_sim_app_path.ts @@ -92,9 +92,7 @@ const baseGetSimulatorAppPathSchema = z.object({ .optional() .describe('Path to .xcworkspace file. Provide EITHER this OR projectPath, not both'), scheme: z.string().describe('The scheme to use (Required)'), - platform: z - .enum(['iOS Simulator', 'watchOS Simulator', 'tvOS Simulator', 'visionOS Simulator']) - .describe('Target simulator platform (Required)'), + platform: z.enum(['iOS Simulator', 'watchOS Simulator', 'tvOS Simulator', 'visionOS Simulator']), simulatorId: z .string() .optional() @@ -305,7 +303,7 @@ const publicSchemaObject = baseGetSimulatorAppPathSchema.omit({ export default { name: 'get_sim_app_path', - description: 'Retrieves the built app path for an iOS simulator.', + description: 'Get sim built app path.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseGetSimulatorAppPathSchema, diff --git a/src/mcp/tools/simulator/install_app_sim.ts b/src/mcp/tools/simulator/install_app_sim.ts index 6d515dd1..98acddab 100644 --- a/src/mcp/tools/simulator/install_app_sim.ts +++ b/src/mcp/tools/simulator/install_app_sim.ts @@ -11,9 +11,7 @@ import { const installAppSimSchemaObject = z.object({ simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'), - appPath: z - .string() - .describe('Path to the .app bundle to install (full path to the .app directory)'), + appPath: z.string(), }); type InstallAppSimParams = z.infer; @@ -98,7 +96,7 @@ export async function install_app_simLogic( export default { name: 'install_app_sim', - description: 'Installs an app in an iOS simulator.', + description: 'Install app on sim.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: installAppSimSchemaObject, diff --git a/src/mcp/tools/simulator/launch_app_logs_sim.ts b/src/mcp/tools/simulator/launch_app_logs_sim.ts index 991acb2d..363b6f72 100644 --- a/src/mcp/tools/simulator/launch_app_logs_sim.ts +++ b/src/mcp/tools/simulator/launch_app_logs_sim.ts @@ -21,10 +21,8 @@ export type LogCaptureFunction = ( const launchAppLogsSimSchemaObject = z.object({ simulatorId: z.string().describe('UUID of the simulator to use (obtained from list_sims)'), - bundleId: z - .string() - .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"), - args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), + bundleId: z.string(), + args: z.array(z.string()).optional(), }); type LaunchAppLogsSimParams = z.infer; @@ -69,7 +67,7 @@ export async function launch_app_logs_simLogic( export default { name: 'launch_app_logs_sim', - description: 'Launches an app in an iOS simulator and captures its logs.', + description: 'Launch sim app with logs.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: launchAppLogsSimSchemaObject, diff --git a/src/mcp/tools/simulator/launch_app_sim.ts b/src/mcp/tools/simulator/launch_app_sim.ts index d3f18bdf..fe9a0952 100644 --- a/src/mcp/tools/simulator/launch_app_sim.ts +++ b/src/mcp/tools/simulator/launch_app_sim.ts @@ -22,10 +22,8 @@ const baseSchemaObject = z.object({ .describe( "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both", ), - bundleId: z - .string() - .describe("Bundle identifier of the app to launch (e.g., 'com.example.MyApp')"), - args: z.array(z.string()).optional().describe('Additional arguments to pass to the app'), + bundleId: z.string(), + args: z.array(z.string()).optional(), }); const launchAppSimSchema = z.preprocess( @@ -203,7 +201,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'launch_app_sim', - description: 'Launches an app in an iOS simulator.', + description: 'Launch app on simulator.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/simulator/list_sims.ts b/src/mcp/tools/simulator/list_sims.ts index e24edfda..0969d36a 100644 --- a/src/mcp/tools/simulator/list_sims.ts +++ b/src/mcp/tools/simulator/list_sims.ts @@ -7,7 +7,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const listSimsSchema = z.object({ - enabled: z.boolean().optional().describe('Optional flag to enable the listing operation.'), + enabled: z.boolean().optional(), }); // Use z.infer for type safety @@ -217,7 +217,7 @@ export async function list_simsLogic( export default { name: 'list_sims', - description: 'Lists available iOS simulators with their UUIDs. ', + description: 'List iOS simulators.', schema: listSimsSchema.shape, // MCP SDK compatibility annotations: { title: 'List Simulators', diff --git a/src/mcp/tools/simulator/open_sim.ts b/src/mcp/tools/simulator/open_sim.ts index 65cd12dd..fdb8cb1d 100644 --- a/src/mcp/tools/simulator/open_sim.ts +++ b/src/mcp/tools/simulator/open_sim.ts @@ -69,7 +69,7 @@ export async function open_simLogic( export default { name: 'open_sim', - description: 'Opens the iOS Simulator app.', + description: 'Open Simulator app.', schema: openSimSchema.shape, // MCP SDK compatibility annotations: { title: 'Open Simulator', diff --git a/src/mcp/tools/simulator/record_sim_video.ts b/src/mcp/tools/simulator/record_sim_video.ts index 4d416f60..62c9a25b 100644 --- a/src/mcp/tools/simulator/record_sim_video.ts +++ b/src/mcp/tools/simulator/record_sim_video.ts @@ -26,13 +26,10 @@ const recordSimVideoSchemaObject = z.object({ simulatorId: z .uuid({ message: 'Invalid Simulator UUID format' }) .describe('UUID of the simulator to record'), - start: z.boolean().optional().describe('Start recording if true'), - stop: z.boolean().optional().describe('Stop recording if true'), - fps: z.number().int().min(1).max(120).optional().describe('Frames per second (default 30)'), - outputFile: z - .string() - .optional() - .describe('Destination MP4 path to move the recorded video to on stop'), + start: z.boolean().optional(), + stop: z.boolean().optional(), + fps: z.number().int().min(1).max(120).optional().describe('default: 30'), + outputFile: z.string().optional().describe('Path to write MP4 file'), }); // Schema enforcing mutually exclusive start/stop and requiring outputFile on stop @@ -226,7 +223,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'record_sim_video', - description: 'Starts or stops video capture for an iOS simulator.', + description: 'Record sim video.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: recordSimVideoSchemaObject, diff --git a/src/mcp/tools/simulator/stop_app_sim.ts b/src/mcp/tools/simulator/stop_app_sim.ts index 172ce07a..2f5efc44 100644 --- a/src/mcp/tools/simulator/stop_app_sim.ts +++ b/src/mcp/tools/simulator/stop_app_sim.ts @@ -22,7 +22,7 @@ const baseSchemaObject = z.object({ .describe( "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both", ), - bundleId: z.string().describe("Bundle identifier of the app to stop (e.g., 'com.example.MyApp')"), + bundleId: z.string(), }); const stopAppSimSchema = z.preprocess( @@ -157,7 +157,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'stop_app_sim', - description: 'Stops an app running in an iOS simulator.', + description: 'Stop sim app.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/simulator/test_sim.ts b/src/mcp/tools/simulator/test_sim.ts index 66d40a3d..d1dfb609 100644 --- a/src/mcp/tools/simulator/test_sim.ts +++ b/src/mcp/tools/simulator/test_sim.ts @@ -43,21 +43,13 @@ const baseSchemaObject = z.object({ "Name of the simulator (e.g., 'iPhone 16'). Provide EITHER this OR simulatorId, not both", ), configuration: z.string().optional().describe('Build configuration (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Path where build products and other derived data will go'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), useLatestOS: z .boolean() .optional() .describe('Whether to use the latest OS version for the named simulator'), - preferXcodebuild: z - .boolean() - .optional() - .describe( - 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', - ), + preferXcodebuild: z.boolean().optional(), testRunnerEnv: z .record(z.string(), z.string()) .optional() @@ -130,7 +122,7 @@ const publicSchemaObject = baseSchemaObject.omit({ export default { name: 'test_sim', - description: 'Runs tests on an iOS simulator.', + description: 'Test on iOS sim.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts index 3fc12358..f0a746a8 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts @@ -21,7 +21,7 @@ describe('swift_package_build plugin', () => { }); it('should have correct description', () => { - expect(swiftPackageBuild.description).toBe('Builds a Swift Package with swift build'); + expect(swiftPackageBuild.description).toBe('swift package target build.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts index 328cadbf..1c24ad84 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_clean.test.ts @@ -21,9 +21,7 @@ describe('swift_package_clean plugin', () => { }); it('should have correct description', () => { - expect(swiftPackageClean.description).toBe( - 'Cleans Swift Package build artifacts and derived data', - ); + expect(swiftPackageClean.description).toBe('swift package clean.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts index 0af914b3..17f8be76 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_list.test.ts @@ -16,7 +16,7 @@ describe('swift_package_list plugin', () => { }); it('should have correct description', () => { - expect(swiftPackageList.description).toBe('Lists currently running Swift Package processes'); + expect(swiftPackageList.description).toBe('List SwiftPM processes.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts index 15d4211d..ca823f9b 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts @@ -21,9 +21,7 @@ describe('swift_package_run plugin', () => { }); it('should have correct description', () => { - expect(swiftPackageRun.description).toBe( - 'Runs an executable target from a Swift Package with swift run', - ); + expect(swiftPackageRun.description).toBe('swift package target run.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_stop.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_stop.test.ts index 0df124d1..de300a14 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_stop.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_stop.test.ts @@ -56,9 +56,7 @@ describe('swift_package_stop plugin', () => { }); it('should have correct description', () => { - expect(swiftPackageStop.description).toBe( - 'Stops a running Swift Package executable started with swift_package_run', - ); + expect(swiftPackageStop.description).toBe('Stop SwiftPM run.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts index 0112e2df..3343dfd2 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts @@ -21,7 +21,7 @@ describe('swift_package_test plugin', () => { }); it('should have correct description', () => { - expect(swiftPackageTest.description).toBe('Runs tests for a Swift Package with swift test'); + expect(swiftPackageTest.description).toBe('Run swift package target tests.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/swift-package/swift_package_build.ts b/src/mcp/tools/swift-package/swift_package_build.ts index 3b7fa1d1..95c0d585 100644 --- a/src/mcp/tools/swift-package/swift_package_build.ts +++ b/src/mcp/tools/swift-package/swift_package_build.ts @@ -9,14 +9,11 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageBuildSchema = z.object({ - packagePath: z.string().describe('Path to the Swift package root (Required)'), - targetName: z.string().optional().describe('Optional target to build'), - configuration: z - .enum(['debug', 'release']) - .optional() - .describe('Swift package configuration (debug, release)'), - architectures: z.array(z.string()).optional().describe('Target architectures to build for'), - parseAsLibrary: z.boolean().optional().describe('Build as library instead of executable'), + packagePath: z.string(), + targetName: z.string().optional(), + configuration: z.enum(['debug', 'release']).optional(), + architectures: z.array(z.string()).optional(), + parseAsLibrary: z.boolean().optional(), }); // Use z.infer for type safety @@ -75,7 +72,7 @@ export async function swift_package_buildLogic( export default { name: 'swift_package_build', - description: 'Builds a Swift Package with swift build', + description: 'swift package target build.', schema: swiftPackageBuildSchema.shape, // MCP SDK compatibility annotations: { title: 'Swift Package Build', diff --git a/src/mcp/tools/swift-package/swift_package_clean.ts b/src/mcp/tools/swift-package/swift_package_clean.ts index 1d973526..0a6195c4 100644 --- a/src/mcp/tools/swift-package/swift_package_clean.ts +++ b/src/mcp/tools/swift-package/swift_package_clean.ts @@ -9,7 +9,7 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageCleanSchema = z.object({ - packagePath: z.string().describe('Path to the Swift package root (Required)'), + packagePath: z.string(), }); // Use z.infer for type safety @@ -50,7 +50,7 @@ export async function swift_package_cleanLogic( export default { name: 'swift_package_clean', - description: 'Cleans Swift Package build artifacts and derived data', + description: 'swift package clean.', schema: swiftPackageCleanSchema.shape, // MCP SDK compatibility annotations: { title: 'Swift Package Clean', diff --git a/src/mcp/tools/swift-package/swift_package_list.ts b/src/mcp/tools/swift-package/swift_package_list.ts index abd1d948..9afc368c 100644 --- a/src/mcp/tools/swift-package/swift_package_list.ts +++ b/src/mcp/tools/swift-package/swift_package_list.ts @@ -77,7 +77,7 @@ type SwiftPackageListParams = z.infer; export default { name: 'swift_package_list', - description: 'Lists currently running Swift Package processes', + description: 'List SwiftPM processes.', schema: swiftPackageListSchema.shape, // MCP SDK compatibility annotations: { title: 'Swift Package List', diff --git a/src/mcp/tools/swift-package/swift_package_run.ts b/src/mcp/tools/swift-package/swift_package_run.ts index c4dbe83e..d6f62c94 100644 --- a/src/mcp/tools/swift-package/swift_package_run.ts +++ b/src/mcp/tools/swift-package/swift_package_run.ts @@ -10,25 +10,13 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageRunSchema = z.object({ - packagePath: z.string().describe('Path to the Swift package root (Required)'), - executableName: z - .string() - .optional() - .describe('Name of executable to run (defaults to package name)'), - arguments: z.array(z.string()).optional().describe('Arguments to pass to the executable'), - configuration: z - .enum(['debug', 'release']) - .optional() - .describe("Build configuration: 'debug' (default) or 'release'"), - timeout: z.number().optional().describe('Timeout in seconds (default: 30, max: 300)'), - background: z - .boolean() - .optional() - .describe('Run in background and return immediately (default: false)'), - parseAsLibrary: z - .boolean() - .optional() - .describe('Add -parse-as-library flag for @main support (default: false)'), + packagePath: z.string(), + executableName: z.string().optional(), + arguments: z.array(z.string()).optional(), + configuration: z.enum(['debug', 'release']).optional(), + timeout: z.number().optional(), + background: z.boolean().optional(), + parseAsLibrary: z.boolean().optional(), }); // Use z.infer for type safety @@ -221,7 +209,7 @@ export async function swift_package_runLogic( export default { name: 'swift_package_run', - description: 'Runs an executable target from a Swift Package with swift run', + description: 'swift package target run.', schema: swiftPackageRunSchema.shape, // MCP SDK compatibility annotations: { title: 'Swift Package Run', diff --git a/src/mcp/tools/swift-package/swift_package_stop.ts b/src/mcp/tools/swift-package/swift_package_stop.ts index f71b5f61..a6c468b5 100644 --- a/src/mcp/tools/swift-package/swift_package_stop.ts +++ b/src/mcp/tools/swift-package/swift_package_stop.ts @@ -5,7 +5,7 @@ import { ToolResponse } from '../../../types/common.ts'; // Define schema as ZodObject const swiftPackageStopSchema = z.object({ - pid: z.number().describe('Process ID (PID) of the running executable'), + pid: z.number(), }); // Use z.infer for type safety @@ -103,7 +103,7 @@ export async function swift_package_stopLogic( export default { name: 'swift_package_stop', - description: 'Stops a running Swift Package executable started with swift_package_run', + description: 'Stop SwiftPM run.', schema: swiftPackageStopSchema.shape, // MCP SDK compatibility annotations: { title: 'Swift Package Stop', diff --git a/src/mcp/tools/swift-package/swift_package_test.ts b/src/mcp/tools/swift-package/swift_package_test.ts index e791592b..44605ac0 100644 --- a/src/mcp/tools/swift-package/swift_package_test.ts +++ b/src/mcp/tools/swift-package/swift_package_test.ts @@ -9,19 +9,13 @@ import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject const swiftPackageTestSchema = z.object({ - packagePath: z.string().describe('Path to the Swift package root (Required)'), - testProduct: z.string().optional().describe('Optional specific test product to run'), - filter: z.string().optional().describe('Filter tests by name (regex pattern)'), - configuration: z - .enum(['debug', 'release']) - .optional() - .describe('Swift package configuration (debug, release)'), - parallel: z.boolean().optional().describe('Run tests in parallel (default: true)'), - showCodecov: z.boolean().optional().describe('Show code coverage (default: false)'), - parseAsLibrary: z - .boolean() - .optional() - .describe('Add -parse-as-library flag for @main support (default: false)'), + packagePath: z.string(), + testProduct: z.string().optional(), + filter: z.string().optional().describe('regex: pattern'), + configuration: z.enum(['debug', 'release']).optional(), + parallel: z.boolean().optional(), + showCodecov: z.boolean().optional(), + parseAsLibrary: z.boolean().optional(), }); // Use z.infer for type safety @@ -88,7 +82,7 @@ export async function swift_package_testLogic( export default { name: 'swift_package_test', - description: 'Runs tests for a Swift Package with swift test', + description: 'Run swift package target tests.', schema: swiftPackageTestSchema.shape, // MCP SDK compatibility annotations: { title: 'Swift Package Test', diff --git a/src/mcp/tools/ui-testing/__tests__/button.test.ts b/src/mcp/tools/ui-testing/__tests__/button.test.ts index bb646102..f990b1f9 100644 --- a/src/mcp/tools/ui-testing/__tests__/button.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/button.test.ts @@ -19,9 +19,7 @@ describe('Button Plugin', () => { }); it('should have correct description', () => { - expect(buttonPlugin.description).toBe( - 'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri', - ); + expect(buttonPlugin.description).toBe('Press simulator hardware button.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts b/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts index aa89e0f1..d97c0bb6 100644 --- a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts @@ -16,7 +16,7 @@ describe('Describe UI Plugin', () => { it('should have correct description', () => { expect(describeUIPlugin.description).toBe( - 'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation. Requires the target process to be running; paused debugger/breakpoints can yield an empty tree.', + 'Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.', ); }); diff --git a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts b/src/mcp/tools/ui-testing/__tests__/gesture.test.ts index 4a15baa7..0c0d8dfe 100644 --- a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/gesture.test.ts @@ -24,9 +24,7 @@ describe('Gesture Plugin', () => { }); it('should have correct description', () => { - expect(gesturePlugin.description).toBe( - 'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge', - ); + expect(gesturePlugin.description).toBe('Simulator gesture preset.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts b/src/mcp/tools/ui-testing/__tests__/key_press.test.ts index 0af1f5c4..b7085a39 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_press.test.ts @@ -25,9 +25,7 @@ describe('Key Press Plugin', () => { }); it('should have correct description', () => { - expect(keyPressPlugin.description).toBe( - 'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.', - ); + expect(keyPressPlugin.description).toBe('Press key by keycode.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts b/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts index 3885f016..c19936af 100644 --- a/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts @@ -23,9 +23,7 @@ describe('Key Sequence Plugin', () => { }); it('should have correct description', () => { - expect(keySequencePlugin.description).toBe( - 'Press key sequence using HID keycodes on iOS simulator with configurable delay', - ); + expect(keySequencePlugin.description).toBe('Press a sequence of keys by their keycodes.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts b/src/mcp/tools/ui-testing/__tests__/long_press.test.ts index 9fc97b4d..123a4c57 100644 --- a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/long_press.test.ts @@ -19,9 +19,7 @@ describe('Long Press Plugin', () => { }); it('should have correct description', () => { - expect(longPressPlugin.description).toBe( - "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).", - ); + expect(longPressPlugin.description).toBe('Long press at coords.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts b/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts index 58c46e51..d8b7fd22 100644 --- a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts @@ -25,9 +25,7 @@ describe('Screenshot Plugin', () => { }); it('should have correct description', () => { - expect(screenshotPlugin.description).toBe( - "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", - ); + expect(screenshotPlugin.description).toBe('Capture screenshot.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts b/src/mcp/tools/ui-testing/__tests__/swipe.test.ts index 041d3628..97c4a4a3 100644 --- a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/swipe.test.ts @@ -59,9 +59,7 @@ describe('Swipe Plugin', () => { }); it('should have correct description', () => { - expect(swipePlugin.description).toBe( - "Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.", - ); + expect(swipePlugin.description).toBe('Swipe between points.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/tap.test.ts b/src/mcp/tools/ui-testing/__tests__/tap.test.ts index d956edf5..a8d0cfac 100644 --- a/src/mcp/tools/ui-testing/__tests__/tap.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/tap.test.ts @@ -54,9 +54,7 @@ describe('Tap Plugin', () => { }); it('should have correct description', () => { - expect(tapPlugin.description).toBe( - "Tap at specific coordinates or target elements by accessibility id or label. Use describe_ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots). Supports optional timing delays.", - ); + expect(tapPlugin.description).toBe('Tap coordinate or element.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/touch.test.ts b/src/mcp/tools/ui-testing/__tests__/touch.test.ts index e6545ec8..78c3237a 100644 --- a/src/mcp/tools/ui-testing/__tests__/touch.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/touch.test.ts @@ -20,9 +20,7 @@ describe('Touch Plugin', () => { }); it('should have correct description', () => { - expect(touchPlugin.description).toBe( - "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).", - ); + expect(touchPlugin.description).toBe('Touch down/up at coords.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts b/src/mcp/tools/ui-testing/__tests__/type_text.test.ts index 53c48d19..4d7c986b 100644 --- a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts +++ b/src/mcp/tools/ui-testing/__tests__/type_text.test.ts @@ -54,9 +54,7 @@ describe('Type Text Plugin', () => { }); it('should have correct description', () => { - expect(typeTextPlugin.description).toBe( - 'Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.', - ); + expect(typeTextPlugin.description).toBe('Type text.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/ui-testing/button.ts b/src/mcp/tools/ui-testing/button.ts index 9e32d48b..8dab02a6 100644 --- a/src/mcp/tools/ui-testing/button.ts +++ b/src/mcp/tools/ui-testing/button.ts @@ -21,8 +21,14 @@ import { // Define schema as ZodObject const buttonSchema = z.object({ simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }), - buttonType: z.enum(['apple-pay', 'home', 'lock', 'side-button', 'siri']), - duration: z.number().min(0, { message: 'Duration must be non-negative' }).optional(), + buttonType: z + .enum(['apple-pay', 'home', 'lock', 'side-button', 'siri']) + .describe('apple-pay|home|lock|side-button|siri'), + duration: z + .number() + .min(0, { message: 'Duration must be non-negative' }) + .optional() + .describe('seconds'), }); // Use z.infer for type safety @@ -96,8 +102,7 @@ const publicSchemaObject = z.strictObject(buttonSchema.omit({ simulatorId: true export default { name: 'button', - description: - 'Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri', + description: 'Press simulator hardware button.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: buttonSchema, diff --git a/src/mcp/tools/ui-testing/describe_ui.ts b/src/mcp/tools/ui-testing/describe_ui.ts index 3141fad7..b63241b7 100644 --- a/src/mcp/tools/ui-testing/describe_ui.ts +++ b/src/mcp/tools/ui-testing/describe_ui.ts @@ -132,7 +132,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'describe_ui', description: - 'Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation. Requires the target process to be running; paused debugger/breakpoints can yield an empty tree.', + 'Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: describeUiSchema, diff --git a/src/mcp/tools/ui-testing/gesture.ts b/src/mcp/tools/ui-testing/gesture.ts index dd03a3c5..6564a177 100644 --- a/src/mcp/tools/ui-testing/gesture.ts +++ b/src/mcp/tools/ui-testing/gesture.ts @@ -45,7 +45,7 @@ const gestureSchema = z.object({ 'swipe-from-bottom-edge', ]) .describe( - 'The gesture preset to perform. Must be one of: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge.', + 'scroll-up|scroll-down|scroll-left|scroll-right|swipe-from-left-edge|swipe-from-right-edge|swipe-from-top-edge|swipe-from-bottom-edge', ), screenWidth: z .number() @@ -53,7 +53,7 @@ const gestureSchema = z.object({ .min(1) .optional() .describe( - 'Optional: Screen width in pixels. Used for gesture calculations. Auto-detected if not provided.', + 'Screen width in pixels. Used for gesture calculations. Auto-detected if not provided.', ), screenHeight: z .number() @@ -61,28 +61,28 @@ const gestureSchema = z.object({ .min(1) .optional() .describe( - 'Optional: Screen height in pixels. Used for gesture calculations. Auto-detected if not provided.', + 'Screen height in pixels. Used for gesture calculations. Auto-detected if not provided.', ), duration: z .number() .min(0, { message: 'Duration must be non-negative' }) .optional() - .describe('Optional: Duration of the gesture in seconds.'), + .describe('Duration of the gesture in seconds.'), delta: z .number() .min(0, { message: 'Delta must be non-negative' }) .optional() - .describe('Optional: Distance to move in pixels.'), + .describe('Distance to move in pixels.'), preDelay: z .number() .min(0, { message: 'Pre-delay must be non-negative' }) .optional() - .describe('Optional: Delay before starting the gesture in seconds.'), + .describe('Delay before starting the gesture in seconds.'), postDelay: z .number() .min(0, { message: 'Post-delay must be non-negative' }) .optional() - .describe('Optional: Delay after completing the gesture in seconds.'), + .describe('Delay after completing the gesture in seconds.'), }); // Use z.infer for type safety @@ -171,8 +171,7 @@ const publicSchemaObject = z.strictObject(gestureSchema.omit({ simulatorId: true export default { name: 'gesture', - description: - 'Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge', + description: 'Simulator gesture preset.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: gestureSchema, diff --git a/src/mcp/tools/ui-testing/key_press.ts b/src/mcp/tools/ui-testing/key_press.ts index 246bc07e..b40ce04e 100644 --- a/src/mcp/tools/ui-testing/key_press.ts +++ b/src/mcp/tools/ui-testing/key_press.ts @@ -26,8 +26,17 @@ import { // Define schema as ZodObject const keyPressSchema = z.object({ simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }), - keyCode: z.number().int({ message: 'HID keycode to press (0-255)' }).min(0).max(255), - duration: z.number().min(0, { message: 'Duration must be non-negative' }).optional(), + keyCode: z + .number() + .int({ message: 'HID keycode to press (0-255)' }) + .min(0) + .max(255) + .describe('HID keycode'), + duration: z + .number() + .min(0, { message: 'Duration must be non-negative' }) + .optional() + .describe('seconds'), }); // Use z.infer for type safety @@ -103,8 +112,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'key_press', - description: - 'Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.', + description: 'Press key by keycode.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: keyPressSchema, diff --git a/src/mcp/tools/ui-testing/key_sequence.ts b/src/mcp/tools/ui-testing/key_sequence.ts index 5707c5f1..57eb952c 100644 --- a/src/mcp/tools/ui-testing/key_sequence.ts +++ b/src/mcp/tools/ui-testing/key_sequence.ts @@ -34,7 +34,8 @@ const keySequenceSchema = z.object({ simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }), keyCodes: z .array(z.number().int().min(0).max(255)) - .min(1, { message: 'At least one key code required' }), + .min(1, { message: 'At least one key code required' }) + .describe('HID keycodes'), delay: z.number().min(0, { message: 'Delay must be non-negative' }).optional(), }); @@ -114,7 +115,7 @@ const publicSchemaObject = z.strictObject( export default { name: 'key_sequence', - description: 'Press key sequence using HID keycodes on iOS simulator with configurable delay', + description: 'Press a sequence of keys by their keycodes.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: keySequenceSchema, diff --git a/src/mcp/tools/ui-testing/long_press.ts b/src/mcp/tools/ui-testing/long_press.ts index eead5e5c..bd7954ea 100644 --- a/src/mcp/tools/ui-testing/long_press.ts +++ b/src/mcp/tools/ui-testing/long_press.ts @@ -35,7 +35,10 @@ const longPressSchema = z.object({ simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }), x: z.number().int({ message: 'X coordinate for the long press' }), y: z.number().int({ message: 'Y coordinate for the long press' }), - duration: z.number().positive({ message: 'Duration of the long press in milliseconds' }), + duration: z + .number() + .positive({ message: 'Duration of the long press in milliseconds' }) + .describe('milliseconds'), }); // Use z.infer for type safety @@ -128,8 +131,7 @@ export async function long_pressLogic( export default { name: 'long_press', - description: - "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).", + description: 'Long press at coords.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: longPressSchema, diff --git a/src/mcp/tools/ui-testing/screenshot.ts b/src/mcp/tools/ui-testing/screenshot.ts index e07d582c..383544da 100644 --- a/src/mcp/tools/ui-testing/screenshot.ts +++ b/src/mcp/tools/ui-testing/screenshot.ts @@ -146,8 +146,7 @@ export async function screenshotLogic( export default { name: 'screenshot', - description: - "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", + description: 'Capture screenshot.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: screenshotSchema, diff --git a/src/mcp/tools/ui-testing/swipe.ts b/src/mcp/tools/ui-testing/swipe.ts index 30343300..f9beda26 100644 --- a/src/mcp/tools/ui-testing/swipe.ts +++ b/src/mcp/tools/ui-testing/swipe.ts @@ -31,10 +31,22 @@ const swipeSchema = z.object({ y1: z.number().int({ message: 'Start Y coordinate' }), x2: z.number().int({ message: 'End X coordinate' }), y2: z.number().int({ message: 'End Y coordinate' }), - duration: z.number().min(0, { message: 'Duration must be non-negative' }).optional(), + duration: z + .number() + .min(0, { message: 'Duration must be non-negative' }) + .optional() + .describe('seconds'), delta: z.number().min(0, { message: 'Delta must be non-negative' }).optional(), - preDelay: z.number().min(0, { message: 'Pre-delay must be non-negative' }).optional(), - postDelay: z.number().min(0, { message: 'Post-delay must be non-negative' }).optional(), + preDelay: z + .number() + .min(0, { message: 'Pre-delay must be non-negative' }) + .optional() + .describe('seconds'), + postDelay: z + .number() + .min(0, { message: 'Post-delay must be non-negative' }) + .optional() + .describe('seconds'), }); // Use z.infer for type safety @@ -136,8 +148,7 @@ export async function swipeLogic( export default { name: 'swipe', - description: - "Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing.", + description: 'Swipe between points.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: swipeSchema, diff --git a/src/mcp/tools/ui-testing/tap.ts b/src/mcp/tools/ui-testing/tap.ts index 82533ae6..7104c9c0 100644 --- a/src/mcp/tools/ui-testing/tap.ts +++ b/src/mcp/tools/ui-testing/tap.ts @@ -31,8 +31,16 @@ const baseTapSchema = z.object({ y: z.number().int({ message: 'Y coordinate must be an integer' }).optional(), id: z.string().min(1, { message: 'Id must be non-empty' }).optional(), label: z.string().min(1, { message: 'Label must be non-empty' }).optional(), - preDelay: z.number().min(0, { message: 'Pre-delay must be non-negative' }).optional(), - postDelay: z.number().min(0, { message: 'Post-delay must be non-negative' }).optional(), + preDelay: z + .number() + .min(0, { message: 'Pre-delay must be non-negative' }) + .optional() + .describe('seconds'), + postDelay: z + .number() + .min(0, { message: 'Post-delay must be non-negative' }) + .optional() + .describe('seconds'), }); const tapSchema = baseTapSchema.superRefine((values, ctx) => { @@ -192,8 +200,7 @@ export async function tapLogic( export default { name: 'tap', - description: - "Tap at specific coordinates or target elements by accessibility id or label. Use describe_ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots). Supports optional timing delays.", + description: 'Tap coordinate or element.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseTapSchema, diff --git a/src/mcp/tools/ui-testing/touch.ts b/src/mcp/tools/ui-testing/touch.ts index e1094e77..374595aa 100644 --- a/src/mcp/tools/ui-testing/touch.ts +++ b/src/mcp/tools/ui-testing/touch.ts @@ -32,7 +32,11 @@ const touchSchema = z.object({ y: z.number().int({ message: 'Y coordinate must be an integer' }), down: z.boolean().optional(), up: z.boolean().optional(), - delay: z.number().min(0, { message: 'Delay must be non-negative' }).optional(), + delay: z + .number() + .min(0, { message: 'Delay must be non-negative' }) + .optional() + .describe('seconds'), }); // Use z.infer for type safety @@ -126,8 +130,7 @@ export async function touchLogic( export default { name: 'touch', - description: - "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).", + description: 'Touch down/up at coords.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: touchSchema, diff --git a/src/mcp/tools/ui-testing/type_text.ts b/src/mcp/tools/ui-testing/type_text.ts index 71019bf1..3e675d77 100644 --- a/src/mcp/tools/ui-testing/type_text.ts +++ b/src/mcp/tools/ui-testing/type_text.ts @@ -103,8 +103,7 @@ export async function type_textLogic( export default { name: 'type_text', - description: - 'Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.', + description: 'Type text.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: typeTextSchema, diff --git a/src/mcp/tools/utilities/__tests__/clean.test.ts b/src/mcp/tools/utilities/__tests__/clean.test.ts index f406ccb1..c742a739 100644 --- a/src/mcp/tools/utilities/__tests__/clean.test.ts +++ b/src/mcp/tools/utilities/__tests__/clean.test.ts @@ -14,7 +14,7 @@ describe('clean (unified) tool', () => { it('exports correct name/description/schema/handler', () => { expect(tool.name).toBe('clean'); - expect(tool.description).toBe('Cleans build products with xcodebuild.'); + expect(tool.description).toBe('Clean build products.'); expect(typeof tool.handler).toBe('function'); const schema = z.strictObject(tool.schema); diff --git a/src/mcp/tools/utilities/clean.ts b/src/mcp/tools/utilities/clean.ts index e701e74b..40361efa 100644 --- a/src/mcp/tools/utilities/clean.ts +++ b/src/mcp/tools/utilities/clean.ts @@ -24,17 +24,9 @@ const baseOptions = { .string() .optional() .describe('Optional: Build configuration to clean (Debug, Release, etc.)'), - derivedDataPath: z - .string() - .optional() - .describe('Optional: Path where derived data might be located'), - extraArgs: z.array(z.string()).optional().describe('Additional xcodebuild arguments'), - preferXcodebuild: z - .boolean() - .optional() - .describe( - 'If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails.', - ), + derivedDataPath: z.string().optional(), + extraArgs: z.array(z.string()).optional(), + preferXcodebuild: z.boolean().optional(), platform: z .enum([ 'macOS', @@ -47,10 +39,7 @@ const baseOptions = { 'visionOS', 'visionOS Simulator', ]) - .optional() - .describe( - 'Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator', - ), + .optional(), }; const baseSchemaObject = z.object({ @@ -159,7 +148,7 @@ const publicSchemaObject = baseSchemaObject.omit({ export default { name: 'clean', - description: 'Cleans build products with xcodebuild.', + description: 'Clean build products.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, legacy: baseSchemaObject, diff --git a/tools.compact.json b/tools.compact.json new file mode 100644 index 00000000..e5704d1b --- /dev/null +++ b/tools.compact.json @@ -0,0 +1,1146 @@ +{ + "mode": "schema-audit", + "toolCount": 71, + "tools": [ + { + "name": "boot_sim", + "description": "Boot iOS simulator.", + "args": [] + }, + { + "name": "build_device", + "description": "Build for device.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_macos", + "description": "Build macOS app.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_run_macos", + "description": "Build and run macOS app.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_run_sim", + "description": "Build and run iOS sim.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_sim", + "description": "Build for iOS sim.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "button", + "description": "Press simulator hardware button.", + "args": [ + { + "name": "buttonType", + "description": "apple-pay|home|lock|side-button|siri" + }, + { + "name": "duration", + "description": "seconds" + } + ] + }, + { + "name": "clean", + "description": "Clean build products.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "platform", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "debug_attach_sim", + "description": "Attach LLDB to sim app.", + "args": [ + { + "name": "bundleId", + "description": null + }, + { + "name": "continueOnAttach", + "description": "default: true" + }, + { + "name": "makeCurrent", + "description": "Set debug session as current (default: true)" + }, + { + "name": "pid", + "description": null + }, + { + "name": "waitFor", + "description": "Wait for the process to appear when attaching" + } + ] + }, + { + "name": "debug_breakpoint_add", + "description": "Add breakpoint.", + "args": [ + { + "name": "condition", + "description": "Expression for breakpoint condition" + }, + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "file", + "description": null + }, + { + "name": "function", + "description": null + }, + { + "name": "line", + "description": null + } + ] + }, + { + "name": "debug_breakpoint_remove", + "description": "Remove breakpoint.", + "args": [ + { + "name": "breakpointId", + "description": null + }, + { + "name": "debugSessionId", + "description": "default: current session" + } + ] + }, + { + "name": "debug_continue", + "description": "Continue debug session.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + } + ] + }, + { + "name": "debug_detach", + "description": "Detach debugger.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + } + ] + }, + { + "name": "debug_lldb_command", + "description": "Run LLDB command.", + "args": [ + { + "name": "command", + "description": null + }, + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "timeoutMs", + "description": null + } + ] + }, + { + "name": "debug_stack", + "description": "Get backtrace.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "maxFrames", + "description": null + }, + { + "name": "threadIndex", + "description": null + } + ] + }, + { + "name": "debug_variables", + "description": "Get frame variables.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "frameIndex", + "description": null + } + ] + }, + { + "name": "describe_ui", + "description": "Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.", + "args": [] + }, + { + "name": "discover_projs", + "description": "Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.", + "args": [ + { + "name": "maxDepth", + "description": null + }, + { + "name": "scanPath", + "description": null + }, + { + "name": "workspaceRoot", + "description": null + } + ] + }, + { + "name": "doctor", + "description": "MCP environment info.", + "args": [ + { + "name": "enabled", + "description": null + } + ] + }, + { + "name": "erase_sims", + "description": "Erase simulator.", + "args": [ + { + "name": "shutdownFirst", + "description": null + } + ] + }, + { + "name": "gesture", + "description": "Simulator gesture preset.", + "args": [ + { + "name": "delta", + "description": "Distance to move in pixels." + }, + { + "name": "duration", + "description": "Duration of the gesture in seconds." + }, + { + "name": "postDelay", + "description": "Delay after completing the gesture in seconds." + }, + { + "name": "preDelay", + "description": "Delay before starting the gesture in seconds." + }, + { + "name": "preset", + "description": "scroll-up|scroll-down|scroll-left|scroll-right|swipe-from-left-edge|swipe-from-right-edge|swipe-from-top-edge|swipe-from-bottom-edge" + }, + { + "name": "screenHeight", + "description": "Screen height in pixels. Used for gesture calculations. Auto-detected if not provided." + }, + { + "name": "screenWidth", + "description": "Screen width in pixels. Used for gesture calculations. Auto-detected if not provided." + } + ] + }, + { + "name": "get_app_bundle_id", + "description": "Extract bundle id from .app.", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle" + } + ] + }, + { + "name": "get_device_app_path", + "description": "Get device built app path.", + "args": [ + { + "name": "platform", + "description": "default: iOS" + } + ] + }, + { + "name": "get_mac_app_path", + "description": "Get macOS built app path.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + } + ] + }, + { + "name": "get_mac_bundle_id", + "description": "Extract bundle id from macOS .app.", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle" + } + ] + }, + { + "name": "get_sim_app_path", + "description": "Get sim built app path.", + "args": [ + { + "name": "platform", + "description": null + } + ] + }, + { + "name": "install_app_device", + "description": "Install app on device.", + "args": [ + { + "name": "appPath", + "description": null + } + ] + }, + { + "name": "install_app_sim", + "description": "Install app on sim.", + "args": [ + { + "name": "appPath", + "description": null + } + ] + }, + { + "name": "key_press", + "description": "Press key by keycode.", + "args": [ + { + "name": "duration", + "description": "seconds" + }, + { + "name": "keyCode", + "description": "HID keycode" + } + ] + }, + { + "name": "key_sequence", + "description": "Press a sequence of keys by their keycodes.", + "args": [ + { + "name": "delay", + "description": null + }, + { + "name": "keyCodes", + "description": "HID keycodes" + } + ] + }, + { + "name": "launch_app_device", + "description": "Launch app on device.", + "args": [ + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "launch_app_logs_sim", + "description": "Launch sim app with logs.", + "args": [ + { + "name": "args", + "description": null + }, + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "launch_app_sim", + "description": "Launch app on simulator.", + "args": [ + { + "name": "args", + "description": null + }, + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "launch_mac_app", + "description": "Launch macOS app.", + "args": [ + { + "name": "appPath", + "description": null + }, + { + "name": "args", + "description": null + } + ] + }, + { + "name": "list_devices", + "description": "List connected devices.", + "args": [] + }, + { + "name": "list_schemes", + "description": "List Xcode schemes.", + "args": [] + }, + { + "name": "list_sims", + "description": "List iOS simulators.", + "args": [ + { + "name": "enabled", + "description": null + } + ] + }, + { + "name": "long_press", + "description": "Long press at coords.", + "args": [ + { + "name": "duration", + "description": "milliseconds" + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "open_sim", + "description": "Open Simulator app.", + "args": [] + }, + { + "name": "record_sim_video", + "description": "Record sim video.", + "args": [ + { + "name": "fps", + "description": "default: 30" + }, + { + "name": "outputFile", + "description": "Path to write MP4 file" + }, + { + "name": "start", + "description": null + }, + { + "name": "stop", + "description": null + } + ] + }, + { + "name": "reset_sim_location", + "description": "Reset sim location.", + "args": [] + }, + { + "name": "scaffold_ios_project", + "description": "Scaffold iOS project.", + "args": [ + { + "name": "bundleIdentifier", + "description": null + }, + { + "name": "currentProjectVersion", + "description": null + }, + { + "name": "customizeNames", + "description": null + }, + { + "name": "deploymentTarget", + "description": null + }, + { + "name": "displayName", + "description": null + }, + { + "name": "marketingVersion", + "description": null + }, + { + "name": "outputPath", + "description": null + }, + { + "name": "projectName", + "description": null + }, + { + "name": "supportedOrientations", + "description": null + }, + { + "name": "supportedOrientationsIpad", + "description": null + }, + { + "name": "targetedDeviceFamily", + "description": null + } + ] + }, + { + "name": "scaffold_macos_project", + "description": "Scaffold macOS project.", + "args": [ + { + "name": "bundleIdentifier", + "description": null + }, + { + "name": "currentProjectVersion", + "description": null + }, + { + "name": "customizeNames", + "description": null + }, + { + "name": "deploymentTarget", + "description": null + }, + { + "name": "displayName", + "description": null + }, + { + "name": "marketingVersion", + "description": null + }, + { + "name": "outputPath", + "description": null + }, + { + "name": "projectName", + "description": null + } + ] + }, + { + "name": "screenshot", + "description": "Capture screenshot.", + "args": [] + }, + { + "name": "session-clear-defaults", + "description": "Clear session defaults.", + "args": [ + { + "name": "all", + "description": null + }, + { + "name": "keys", + "description": null + } + ] + }, + { + "name": "session-set-defaults", + "description": "Set the session defaults, should be called at least once to set tool defaults.", + "args": [ + { + "name": "arch", + "description": null + }, + { + "name": "configuration", + "description": "e.g. 'Debug' or 'Release'." + }, + { + "name": "deviceId", + "description": null + }, + { + "name": "projectPath", + "description": "xcodeproj path (xor workspacePath)" + }, + { + "name": "scheme", + "description": null + }, + { + "name": "simulatorId", + "description": null + }, + { + "name": "simulatorName", + "description": null + }, + { + "name": "suppressWarnings", + "description": null + }, + { + "name": "useLatestOS", + "description": null + }, + { + "name": "workspacePath", + "description": "xcworkspace path (xor projectPath)" + } + ] + }, + { + "name": "session-show-defaults", + "description": "Show session defaults.", + "args": [] + }, + { + "name": "set_sim_appearance", + "description": "Set sim appearance.", + "args": [ + { + "name": "mode", + "description": "dark|light" + } + ] + }, + { + "name": "set_sim_location", + "description": "Set sim location.", + "args": [ + { + "name": "latitude", + "description": null + }, + { + "name": "longitude", + "description": null + } + ] + }, + { + "name": "show_build_settings", + "description": "Show build settings.", + "args": [] + }, + { + "name": "sim_statusbar", + "description": "Set sim status bar network.", + "args": [ + { + "name": "dataNetwork", + "description": "clear|hide|wifi|3g|4g|lte|lte-a|lte+|5g|5g+|5g-uwb|5g-uc" + } + ] + }, + { + "name": "start_device_log_cap", + "description": "Start device log capture.", + "args": [ + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "start_sim_log_cap", + "description": "Start sim log capture.", + "args": [ + { + "name": "bundleId", + "description": null + }, + { + "name": "captureConsole", + "description": null + }, + { + "name": "subsystemFilter", + "description": "app|all|swiftui|[subsystem]" + } + ] + }, + { + "name": "stop_app_device", + "description": "Stop device app.", + "args": [ + { + "name": "processId", + "description": null + } + ] + }, + { + "name": "stop_app_sim", + "description": "Stop sim app.", + "args": [ + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "stop_device_log_cap", + "description": "Stop device log capture.", + "args": [ + { + "name": "logSessionId", + "description": null + } + ] + }, + { + "name": "stop_mac_app", + "description": "Stop macOS app.", + "args": [ + { + "name": "appName", + "description": null + }, + { + "name": "processId", + "description": null + } + ] + }, + { + "name": "stop_sim_log_cap", + "description": "Stop sim log capture.", + "args": [ + { + "name": "logSessionId", + "description": null + } + ] + }, + { + "name": "swift_package_build", + "description": "swift package target build.", + "args": [ + { + "name": "architectures", + "description": null + }, + { + "name": "configuration", + "description": null + }, + { + "name": "packagePath", + "description": null + }, + { + "name": "parseAsLibrary", + "description": null + }, + { + "name": "targetName", + "description": null + } + ] + }, + { + "name": "swift_package_clean", + "description": "swift package clean.", + "args": [ + { + "name": "packagePath", + "description": null + } + ] + }, + { + "name": "swift_package_list", + "description": "List SwiftPM processes.", + "args": [] + }, + { + "name": "swift_package_run", + "description": "swift package target run.", + "args": [ + { + "name": "arguments", + "description": null + }, + { + "name": "background", + "description": null + }, + { + "name": "configuration", + "description": null + }, + { + "name": "executableName", + "description": null + }, + { + "name": "packagePath", + "description": null + }, + { + "name": "parseAsLibrary", + "description": null + }, + { + "name": "timeout", + "description": null + } + ] + }, + { + "name": "swift_package_stop", + "description": "Stop SwiftPM run.", + "args": [ + { + "name": "pid", + "description": null + } + ] + }, + { + "name": "swift_package_test", + "description": "Run swift package target tests.", + "args": [ + { + "name": "configuration", + "description": null + }, + { + "name": "filter", + "description": "regex: pattern" + }, + { + "name": "packagePath", + "description": null + }, + { + "name": "parallel", + "description": null + }, + { + "name": "parseAsLibrary", + "description": null + }, + { + "name": "showCodecov", + "description": null + }, + { + "name": "testProduct", + "description": null + } + ] + }, + { + "name": "swipe", + "description": "Swipe between points.", + "args": [ + { + "name": "delta", + "description": null + }, + { + "name": "duration", + "description": "seconds" + }, + { + "name": "postDelay", + "description": "seconds" + }, + { + "name": "preDelay", + "description": "seconds" + }, + { + "name": "x1", + "description": null + }, + { + "name": "x2", + "description": null + }, + { + "name": "y1", + "description": null + }, + { + "name": "y2", + "description": null + } + ] + }, + { + "name": "tap", + "description": "Tap coordinate or element.", + "args": [ + { + "name": "id", + "description": null + }, + { + "name": "label", + "description": null + }, + { + "name": "postDelay", + "description": "seconds" + }, + { + "name": "preDelay", + "description": "seconds" + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "test_device", + "description": "Test on device.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "platform", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "test_macos", + "description": "Test macOS target.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "test_sim", + "description": "Test on iOS sim.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "touch", + "description": "Touch down/up at coords.", + "args": [ + { + "name": "delay", + "description": "seconds" + }, + { + "name": "down", + "description": null + }, + { + "name": "up", + "description": null + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "type_text", + "description": "Type text.", + "args": [ + { + "name": "text", + "description": null + } + ] + } + ] +} \ No newline at end of file diff --git a/tools.new.json b/tools.new.json new file mode 100644 index 00000000..28b921cb --- /dev/null +++ b/tools.new.json @@ -0,0 +1,1150 @@ + +> xcodebuildmcp@1.15.1 tools +> npx tsx scripts/tools-cli.ts schema --json + +{ + "mode": "schema-audit", + "toolCount": 71, + "tools": [ + { + "name": "boot_sim", + "description": "Boot iOS simulator.", + "args": [] + }, + { + "name": "build_device", + "description": "Build for device.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_macos", + "description": "Build macOS app.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_run_macos", + "description": "Build and run macOS app.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_run_sim", + "description": "Build and run iOS sim.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "build_sim", + "description": "Build for iOS sim.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "button", + "description": "Press simulator hardware button.", + "args": [ + { + "name": "buttonType", + "description": "apple-pay|home|lock|side-button|siri" + }, + { + "name": "duration", + "description": "seconds" + } + ] + }, + { + "name": "clean", + "description": "Clean build products.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "platform", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + } + ] + }, + { + "name": "debug_attach_sim", + "description": "Attach LLDB to sim app.", + "args": [ + { + "name": "bundleId", + "description": null + }, + { + "name": "continueOnAttach", + "description": "default: true" + }, + { + "name": "makeCurrent", + "description": "Set debug session as current (default: true)" + }, + { + "name": "pid", + "description": null + }, + { + "name": "waitFor", + "description": "Wait for the process to appear when attaching" + } + ] + }, + { + "name": "debug_breakpoint_add", + "description": "Add breakpoint.", + "args": [ + { + "name": "condition", + "description": "Expression for breakpoint condition" + }, + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "file", + "description": null + }, + { + "name": "function", + "description": null + }, + { + "name": "line", + "description": null + } + ] + }, + { + "name": "debug_breakpoint_remove", + "description": "Remove breakpoint.", + "args": [ + { + "name": "breakpointId", + "description": null + }, + { + "name": "debugSessionId", + "description": "default: current session" + } + ] + }, + { + "name": "debug_continue", + "description": "Continue debug session.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + } + ] + }, + { + "name": "debug_detach", + "description": "Detach debugger.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + } + ] + }, + { + "name": "debug_lldb_command", + "description": "Run LLDB command.", + "args": [ + { + "name": "command", + "description": null + }, + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "timeoutMs", + "description": null + } + ] + }, + { + "name": "debug_stack", + "description": "Get backtrace.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "maxFrames", + "description": null + }, + { + "name": "threadIndex", + "description": null + } + ] + }, + { + "name": "debug_variables", + "description": "Get frame variables.", + "args": [ + { + "name": "debugSessionId", + "description": "default: current session" + }, + { + "name": "frameIndex", + "description": null + } + ] + }, + { + "name": "describe_ui", + "description": "Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.", + "args": [] + }, + { + "name": "discover_projs", + "description": "Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.", + "args": [ + { + "name": "maxDepth", + "description": null + }, + { + "name": "scanPath", + "description": null + }, + { + "name": "workspaceRoot", + "description": null + } + ] + }, + { + "name": "doctor", + "description": "MCP environment info.", + "args": [ + { + "name": "enabled", + "description": null + } + ] + }, + { + "name": "erase_sims", + "description": "Erase simulator.", + "args": [ + { + "name": "shutdownFirst", + "description": null + } + ] + }, + { + "name": "gesture", + "description": "Simulator gesture preset.", + "args": [ + { + "name": "delta", + "description": "Distance to move in pixels." + }, + { + "name": "duration", + "description": "Duration of the gesture in seconds." + }, + { + "name": "postDelay", + "description": "Delay after completing the gesture in seconds." + }, + { + "name": "preDelay", + "description": "Delay before starting the gesture in seconds." + }, + { + "name": "preset", + "description": "scroll-up|scroll-down|scroll-left|scroll-right|swipe-from-left-edge|swipe-from-right-edge|swipe-from-top-edge|swipe-from-bottom-edge" + }, + { + "name": "screenHeight", + "description": "Screen height in pixels. Used for gesture calculations. Auto-detected if not provided." + }, + { + "name": "screenWidth", + "description": "Screen width in pixels. Used for gesture calculations. Auto-detected if not provided." + } + ] + }, + { + "name": "get_app_bundle_id", + "description": "Extract bundle id from .app.", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle" + } + ] + }, + { + "name": "get_device_app_path", + "description": "Get device built app path.", + "args": [ + { + "name": "platform", + "description": "default: iOS" + } + ] + }, + { + "name": "get_mac_app_path", + "description": "Get macOS built app path.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + } + ] + }, + { + "name": "get_mac_bundle_id", + "description": "Extract bundle id from macOS .app.", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle" + } + ] + }, + { + "name": "get_sim_app_path", + "description": "Get sim built app path.", + "args": [ + { + "name": "platform", + "description": null + } + ] + }, + { + "name": "install_app_device", + "description": "Install app on device.", + "args": [ + { + "name": "appPath", + "description": null + } + ] + }, + { + "name": "install_app_sim", + "description": "Install app on sim.", + "args": [ + { + "name": "appPath", + "description": null + } + ] + }, + { + "name": "key_press", + "description": "Press key by keycode.", + "args": [ + { + "name": "duration", + "description": "seconds" + }, + { + "name": "keyCode", + "description": "HID keycode" + } + ] + }, + { + "name": "key_sequence", + "description": "Press a sequence of keys by their keycodes.", + "args": [ + { + "name": "delay", + "description": null + }, + { + "name": "keyCodes", + "description": "HID keycodes" + } + ] + }, + { + "name": "launch_app_device", + "description": "Launch app on device.", + "args": [ + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "launch_app_logs_sim", + "description": "Launch sim app with logs.", + "args": [ + { + "name": "args", + "description": null + }, + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "launch_app_sim", + "description": "Launch app on simulator.", + "args": [ + { + "name": "args", + "description": null + }, + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "launch_mac_app", + "description": "Launch macOS app.", + "args": [ + { + "name": "appPath", + "description": null + }, + { + "name": "args", + "description": null + } + ] + }, + { + "name": "list_devices", + "description": "List connected devices.", + "args": [] + }, + { + "name": "list_schemes", + "description": "List Xcode schemes.", + "args": [] + }, + { + "name": "list_sims", + "description": "List iOS simulators.", + "args": [ + { + "name": "enabled", + "description": null + } + ] + }, + { + "name": "long_press", + "description": "Long press at coords.", + "args": [ + { + "name": "duration", + "description": "milliseconds" + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "open_sim", + "description": "Open Simulator app.", + "args": [] + }, + { + "name": "record_sim_video", + "description": "Record sim video.", + "args": [ + { + "name": "fps", + "description": "default: 30" + }, + { + "name": "outputFile", + "description": "Path to write MP4 file" + }, + { + "name": "start", + "description": null + }, + { + "name": "stop", + "description": null + } + ] + }, + { + "name": "reset_sim_location", + "description": "Reset sim location.", + "args": [] + }, + { + "name": "scaffold_ios_project", + "description": "Scaffold iOS project.", + "args": [ + { + "name": "bundleIdentifier", + "description": null + }, + { + "name": "currentProjectVersion", + "description": null + }, + { + "name": "customizeNames", + "description": null + }, + { + "name": "deploymentTarget", + "description": null + }, + { + "name": "displayName", + "description": null + }, + { + "name": "marketingVersion", + "description": null + }, + { + "name": "outputPath", + "description": null + }, + { + "name": "projectName", + "description": null + }, + { + "name": "supportedOrientations", + "description": null + }, + { + "name": "supportedOrientationsIpad", + "description": null + }, + { + "name": "targetedDeviceFamily", + "description": null + } + ] + }, + { + "name": "scaffold_macos_project", + "description": "Scaffold macOS project.", + "args": [ + { + "name": "bundleIdentifier", + "description": null + }, + { + "name": "currentProjectVersion", + "description": null + }, + { + "name": "customizeNames", + "description": null + }, + { + "name": "deploymentTarget", + "description": null + }, + { + "name": "displayName", + "description": null + }, + { + "name": "marketingVersion", + "description": null + }, + { + "name": "outputPath", + "description": null + }, + { + "name": "projectName", + "description": null + } + ] + }, + { + "name": "screenshot", + "description": "Capture screenshot.", + "args": [] + }, + { + "name": "session-clear-defaults", + "description": "Clear session defaults.", + "args": [ + { + "name": "all", + "description": null + }, + { + "name": "keys", + "description": null + } + ] + }, + { + "name": "session-set-defaults", + "description": "Set the session defaults, should be called at least once to set tool defaults.", + "args": [ + { + "name": "arch", + "description": null + }, + { + "name": "configuration", + "description": "e.g. 'Debug' or 'Release'." + }, + { + "name": "deviceId", + "description": null + }, + { + "name": "projectPath", + "description": "xcodeproj path (xor workspacePath)" + }, + { + "name": "scheme", + "description": null + }, + { + "name": "simulatorId", + "description": null + }, + { + "name": "simulatorName", + "description": null + }, + { + "name": "suppressWarnings", + "description": null + }, + { + "name": "useLatestOS", + "description": null + }, + { + "name": "workspacePath", + "description": "xcworkspace path (xor projectPath)" + } + ] + }, + { + "name": "session-show-defaults", + "description": "Show session defaults.", + "args": [] + }, + { + "name": "set_sim_appearance", + "description": "Set sim appearance.", + "args": [ + { + "name": "mode", + "description": "dark|light" + } + ] + }, + { + "name": "set_sim_location", + "description": "Set sim location.", + "args": [ + { + "name": "latitude", + "description": null + }, + { + "name": "longitude", + "description": null + } + ] + }, + { + "name": "show_build_settings", + "description": "Show build settings.", + "args": [] + }, + { + "name": "sim_statusbar", + "description": "Set sim status bar network.", + "args": [ + { + "name": "dataNetwork", + "description": "clear|hide|wifi|3g|4g|lte|lte-a|lte+|5g|5g+|5g-uwb|5g-uc" + } + ] + }, + { + "name": "start_device_log_cap", + "description": "Start device log capture.", + "args": [ + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "start_sim_log_cap", + "description": "Start sim log capture.", + "args": [ + { + "name": "bundleId", + "description": null + }, + { + "name": "captureConsole", + "description": null + }, + { + "name": "subsystemFilter", + "description": "app|all|swiftui|[subsystem]" + } + ] + }, + { + "name": "stop_app_device", + "description": "Stop device app.", + "args": [ + { + "name": "processId", + "description": null + } + ] + }, + { + "name": "stop_app_sim", + "description": "Stop sim app.", + "args": [ + { + "name": "bundleId", + "description": null + } + ] + }, + { + "name": "stop_device_log_cap", + "description": "Stop device log capture.", + "args": [ + { + "name": "logSessionId", + "description": null + } + ] + }, + { + "name": "stop_mac_app", + "description": "Stop macOS app.", + "args": [ + { + "name": "appName", + "description": null + }, + { + "name": "processId", + "description": null + } + ] + }, + { + "name": "stop_sim_log_cap", + "description": "Stop sim log capture.", + "args": [ + { + "name": "logSessionId", + "description": null + } + ] + }, + { + "name": "swift_package_build", + "description": "swift package target build.", + "args": [ + { + "name": "architectures", + "description": null + }, + { + "name": "configuration", + "description": null + }, + { + "name": "packagePath", + "description": null + }, + { + "name": "parseAsLibrary", + "description": null + }, + { + "name": "targetName", + "description": null + } + ] + }, + { + "name": "swift_package_clean", + "description": "swift package clean.", + "args": [ + { + "name": "packagePath", + "description": null + } + ] + }, + { + "name": "swift_package_list", + "description": "List SwiftPM processes.", + "args": [] + }, + { + "name": "swift_package_run", + "description": "swift package target run.", + "args": [ + { + "name": "arguments", + "description": null + }, + { + "name": "background", + "description": null + }, + { + "name": "configuration", + "description": null + }, + { + "name": "executableName", + "description": null + }, + { + "name": "packagePath", + "description": null + }, + { + "name": "parseAsLibrary", + "description": null + }, + { + "name": "timeout", + "description": null + } + ] + }, + { + "name": "swift_package_stop", + "description": "Stop SwiftPM run.", + "args": [ + { + "name": "pid", + "description": null + } + ] + }, + { + "name": "swift_package_test", + "description": "Run swift package target tests.", + "args": [ + { + "name": "configuration", + "description": null + }, + { + "name": "filter", + "description": "regex: pattern" + }, + { + "name": "packagePath", + "description": null + }, + { + "name": "parallel", + "description": null + }, + { + "name": "parseAsLibrary", + "description": null + }, + { + "name": "showCodecov", + "description": null + }, + { + "name": "testProduct", + "description": null + } + ] + }, + { + "name": "swipe", + "description": "Swipe between points.", + "args": [ + { + "name": "delta", + "description": null + }, + { + "name": "duration", + "description": "seconds" + }, + { + "name": "postDelay", + "description": "seconds" + }, + { + "name": "preDelay", + "description": "seconds" + }, + { + "name": "x1", + "description": null + }, + { + "name": "x2", + "description": null + }, + { + "name": "y1", + "description": null + }, + { + "name": "y2", + "description": null + } + ] + }, + { + "name": "tap", + "description": "Tap coordinate or element.", + "args": [ + { + "name": "id", + "description": null + }, + { + "name": "label", + "description": null + }, + { + "name": "postDelay", + "description": "seconds" + }, + { + "name": "preDelay", + "description": "seconds" + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "test_device", + "description": "Test on device.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "platform", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "test_macos", + "description": "Test macOS target.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "test_sim", + "description": "Test on iOS sim.", + "args": [ + { + "name": "derivedDataPath", + "description": null + }, + { + "name": "extraArgs", + "description": null + }, + { + "name": "preferXcodebuild", + "description": null + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "touch", + "description": "Touch down/up at coords.", + "args": [ + { + "name": "delay", + "description": "seconds" + }, + { + "name": "down", + "description": null + }, + { + "name": "up", + "description": null + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "type_text", + "description": "Type text.", + "args": [ + { + "name": "text", + "description": null + } + ] + } + ] +} diff --git a/tools_original.json b/tools_original.json new file mode 100644 index 00000000..d516859a --- /dev/null +++ b/tools_original.json @@ -0,0 +1,1146 @@ +{ + "mode": "schema-audit", + "toolCount": 71, + "tools": [ + { + "name": "boot_sim", + "description": "Boots an iOS simulator.", + "args": [] + }, + { + "name": "build_device", + "description": "Builds an app for a connected device.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path to derived data directory" + }, + { + "name": "extraArgs", + "description": "Additional arguments to pass to xcodebuild" + }, + { + "name": "preferXcodebuild", + "description": "Prefer xcodebuild over faster alternatives" + } + ] + }, + { + "name": "build_macos", + "description": "Builds a macOS app.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path where build products and other derived data will go" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system" + } + ] + }, + { + "name": "build_run_macos", + "description": "Builds and runs a macOS app.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path where build products and other derived data will go" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system" + } + ] + }, + { + "name": "build_run_sim", + "description": "Builds and runs an app on an iOS simulator.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path where build products and other derived data will go" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails." + } + ] + }, + { + "name": "build_sim", + "description": "Builds an app for an iOS simulator.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path where build products and other derived data will go" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails." + } + ] + }, + { + "name": "button", + "description": "Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri", + "args": [ + { + "name": "buttonType", + "description": null + }, + { + "name": "duration", + "description": null + } + ] + }, + { + "name": "clean", + "description": "Cleans build products with xcodebuild.", + "args": [ + { + "name": "derivedDataPath", + "description": "Optional: Path where derived data might be located" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "platform", + "description": "Optional: Platform to clean for (defaults to iOS). Choose from macOS, iOS, iOS Simulator, watchOS, watchOS Simulator, tvOS, tvOS Simulator, visionOS, visionOS Simulator" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails." + } + ] + }, + { + "name": "debug_attach_sim", + "description": "Attach LLDB to a running iOS simulator app. Provide bundleId or pid, plus simulator defaults.", + "args": [ + { + "name": "bundleId", + "description": "Bundle identifier of the app to attach (e.g., 'com.example.MyApp')" + }, + { + "name": "continueOnAttach", + "description": "Resume execution automatically after attaching (default: true)" + }, + { + "name": "makeCurrent", + "description": "Set this debug session as the current session (default: true)" + }, + { + "name": "pid", + "description": "Process ID to attach directly" + }, + { + "name": "waitFor", + "description": "Wait for the process to appear when attaching" + } + ] + }, + { + "name": "debug_breakpoint_add", + "description": "Add a breakpoint by file/line or function name for the active debug session.", + "args": [ + { + "name": "condition", + "description": "Optional condition expression for the breakpoint" + }, + { + "name": "debugSessionId", + "description": "Debug session ID to target (defaults to current session)" + }, + { + "name": "file", + "description": null + }, + { + "name": "function", + "description": "Function name to break on" + }, + { + "name": "line", + "description": "Line number for breakpoint" + } + ] + }, + { + "name": "debug_breakpoint_remove", + "description": "Remove a breakpoint by id for the active debug session.", + "args": [ + { + "name": "breakpointId", + "description": "Breakpoint id to remove" + }, + { + "name": "debugSessionId", + "description": "Debug session ID to target (defaults to current session)" + } + ] + }, + { + "name": "debug_continue", + "description": "Resume execution in the active debug session or a specific debugSessionId.", + "args": [ + { + "name": "debugSessionId", + "description": "Debug session ID to resume (defaults to current session)" + } + ] + }, + { + "name": "debug_detach", + "description": "Detach the current debugger session or a specific debugSessionId.", + "args": [ + { + "name": "debugSessionId", + "description": "Debug session ID to detach (defaults to current session)" + } + ] + }, + { + "name": "debug_lldb_command", + "description": "Run an arbitrary LLDB command within the active debug session.", + "args": [ + { + "name": "command", + "description": null + }, + { + "name": "debugSessionId", + "description": null + }, + { + "name": "timeoutMs", + "description": null + } + ] + }, + { + "name": "debug_stack", + "description": "Return a thread backtrace from the active debug session.", + "args": [ + { + "name": "debugSessionId", + "description": "Debug session ID to target (defaults to current session)" + }, + { + "name": "maxFrames", + "description": "Maximum frames to return" + }, + { + "name": "threadIndex", + "description": "Thread index for backtrace" + } + ] + }, + { + "name": "debug_variables", + "description": "Return variables for a selected frame in the active debug session.", + "args": [ + { + "name": "debugSessionId", + "description": "Debug session ID to target (defaults to current session)" + }, + { + "name": "frameIndex", + "description": "Frame index to inspect" + } + ] + }, + { + "name": "describe_ui", + "description": "Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation. Requires the target process to be running; paused debugger/breakpoints can yield an empty tree.", + "args": [] + }, + { + "name": "discover_projs", + "description": "Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files.", + "args": [ + { + "name": "maxDepth", + "description": "Optional: Maximum directory depth to scan. Defaults to ${DEFAULT_MAX_DEPTH}." + }, + { + "name": "scanPath", + "description": "Optional: Path relative to workspace root to scan. Defaults to workspace root." + }, + { + "name": "workspaceRoot", + "description": "The absolute path of the workspace root to scan within." + } + ] + }, + { + "name": "doctor", + "description": "Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.", + "args": [ + { + "name": "enabled", + "description": "Optional: dummy parameter to satisfy MCP protocol" + } + ] + }, + { + "name": "erase_sims", + "description": "Erases a simulator by UDID.", + "args": [ + { + "name": "shutdownFirst", + "description": "If true, shuts down the simulator before erasing." + } + ] + }, + { + "name": "gesture", + "description": "Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge", + "args": [ + { + "name": "delta", + "description": "Optional: Distance to move in pixels." + }, + { + "name": "duration", + "description": "Optional: Duration of the gesture in seconds." + }, + { + "name": "postDelay", + "description": "Optional: Delay after completing the gesture in seconds." + }, + { + "name": "preDelay", + "description": "Optional: Delay before starting the gesture in seconds." + }, + { + "name": "preset", + "description": "The gesture preset to perform. Must be one of: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge." + }, + { + "name": "screenHeight", + "description": "Optional: Screen height in pixels. Used for gesture calculations. Auto-detected if not provided." + }, + { + "name": "screenWidth", + "description": "Optional: Screen width in pixels. Used for gesture calculations. Auto-detected if not provided." + } + ] + }, + { + "name": "get_app_bundle_id", + "description": "Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS). IMPORTANT: You MUST provide the appPath parameter. Example: get_app_bundle_id({ appPath: '/path/to/your/app.app' })", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle to extract bundle ID from (full path to the .app directory)" + } + ] + }, + { + "name": "get_device_app_path", + "description": "Retrieves the built app path for a connected device.", + "args": [ + { + "name": "platform", + "description": "Target platform (defaults to iOS)" + } + ] + }, + { + "name": "get_mac_app_path", + "description": "Retrieves the built macOS app bundle path.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path to derived data directory" + }, + { + "name": "extraArgs", + "description": "Additional arguments to pass to xcodebuild" + } + ] + }, + { + "name": "get_mac_bundle_id", + "description": "Extracts the bundle identifier from a macOS app bundle (.app). IMPORTANT: You MUST provide the appPath parameter. Example: get_mac_bundle_id({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id.", + "args": [ + { + "name": "appPath", + "description": "Path to the macOS .app bundle to extract bundle ID from (full path to the .app directory)" + } + ] + }, + { + "name": "get_sim_app_path", + "description": "Retrieves the built app path for an iOS simulator.", + "args": [ + { + "name": "platform", + "description": "Target simulator platform (Required)" + } + ] + }, + { + "name": "install_app_device", + "description": "Installs an app on a connected device.", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle to install (full path to the .app directory)" + } + ] + }, + { + "name": "install_app_sim", + "description": "Installs an app in an iOS simulator.", + "args": [ + { + "name": "appPath", + "description": "Path to the .app bundle to install (full path to the .app directory)" + } + ] + }, + { + "name": "key_press", + "description": "Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10.", + "args": [ + { + "name": "duration", + "description": null + }, + { + "name": "keyCode", + "description": null + } + ] + }, + { + "name": "key_sequence", + "description": "Press key sequence using HID keycodes on iOS simulator with configurable delay", + "args": [ + { + "name": "delay", + "description": null + }, + { + "name": "keyCodes", + "description": null + } + ] + }, + { + "name": "launch_app_device", + "description": "Launches an app on a connected device.", + "args": [ + { + "name": "bundleId", + "description": "Bundle identifier of the app to launch (e.g., \"com.example.MyApp\")" + } + ] + }, + { + "name": "launch_app_logs_sim", + "description": "Launches an app in an iOS simulator and captures its logs.", + "args": [ + { + "name": "args", + "description": "Additional arguments to pass to the app" + }, + { + "name": "bundleId", + "description": "Bundle identifier of the app to launch (e.g., 'com.example.MyApp')" + } + ] + }, + { + "name": "launch_app_sim", + "description": "Launches an app in an iOS simulator.", + "args": [ + { + "name": "args", + "description": "Additional arguments to pass to the app" + }, + { + "name": "bundleId", + "description": "Bundle identifier of the app to launch (e.g., 'com.example.MyApp')" + } + ] + }, + { + "name": "launch_mac_app", + "description": "Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.", + "args": [ + { + "name": "appPath", + "description": "Path to the macOS .app bundle to launch (full path to the .app directory)" + }, + { + "name": "args", + "description": "Additional arguments to pass to the app" + } + ] + }, + { + "name": "list_devices", + "description": "Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.", + "args": [] + }, + { + "name": "list_schemes", + "description": "Lists schemes for a project or workspace.", + "args": [] + }, + { + "name": "list_sims", + "description": "Lists available iOS simulators with their UUIDs.", + "args": [ + { + "name": "enabled", + "description": "Optional flag to enable the listing operation." + } + ] + }, + { + "name": "long_press", + "description": "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).", + "args": [ + { + "name": "duration", + "description": null + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "open_sim", + "description": "Opens the iOS Simulator app.", + "args": [] + }, + { + "name": "record_sim_video", + "description": "Starts or stops video capture for an iOS simulator.", + "args": [ + { + "name": "fps", + "description": "Frames per second (default 30)" + }, + { + "name": "outputFile", + "description": "Destination MP4 path to move the recorded video to on stop" + }, + { + "name": "start", + "description": "Start recording if true" + }, + { + "name": "stop", + "description": "Stop recording if true" + } + ] + }, + { + "name": "reset_sim_location", + "description": "Resets the simulator's location to default.", + "args": [] + }, + { + "name": "scaffold_ios_project", + "description": "Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration.", + "args": [ + { + "name": "bundleIdentifier", + "description": "Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname" + }, + { + "name": "currentProjectVersion", + "description": "Build number (e.g., 1, 42, 100). If not provided, will use 1" + }, + { + "name": "customizeNames", + "description": "Whether to customize project names and identifiers. Default is true." + }, + { + "name": "deploymentTarget", + "description": "iOS deployment target (e.g., 18.4, 17.0). If not provided, will use 18.4" + }, + { + "name": "displayName", + "description": "App display name (shown on home screen/dock). If not provided, will use projectName" + }, + { + "name": "marketingVersion", + "description": "Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0" + }, + { + "name": "outputPath", + "description": "Path where the project should be created" + }, + { + "name": "projectName", + "description": "Name of the new project" + }, + { + "name": "supportedOrientations", + "description": "Supported orientations for iPhone" + }, + { + "name": "supportedOrientationsIpad", + "description": "Supported orientations for iPad" + }, + { + "name": "targetedDeviceFamily", + "description": "Targeted device families" + } + ] + }, + { + "name": "scaffold_macos_project", + "description": "Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.", + "args": [ + { + "name": "bundleIdentifier", + "description": "Bundle identifier (e.g., com.example.myapp). If not provided, will use com.example.projectname" + }, + { + "name": "currentProjectVersion", + "description": "Build number (e.g., 1, 42, 100). If not provided, will use 1" + }, + { + "name": "customizeNames", + "description": "Whether to customize project names and identifiers. Default is true." + }, + { + "name": "deploymentTarget", + "description": "macOS deployment target (e.g., 15.4, 14.0). If not provided, will use 15.4" + }, + { + "name": "displayName", + "description": "App display name (shown on home screen/dock). If not provided, will use projectName" + }, + { + "name": "marketingVersion", + "description": "Marketing version (e.g., 1.0, 2.1.3). If not provided, will use 1.0" + }, + { + "name": "outputPath", + "description": "Path where the project should be created" + }, + { + "name": "projectName", + "description": "Name of the new project" + } + ] + }, + { + "name": "screenshot", + "description": "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", + "args": [] + }, + { + "name": "session-clear-defaults", + "description": "Clear selected or all session defaults.", + "args": [ + { + "name": "all", + "description": null + }, + { + "name": "keys", + "description": null + } + ] + }, + { + "name": "session-set-defaults", + "description": "Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set all relevant defaults up front in a single call (e.g., project/workspace, scheme, simulator or device ID, useLatestOS) to avoid iterative prompts; only set the keys your workflow needs.", + "args": [ + { + "name": "arch", + "description": "Target architecture for macOS builds." + }, + { + "name": "configuration", + "description": "Build configuration, e.g. Debug or Release." + }, + { + "name": "deviceId", + "description": "Physical device ID for device workflows." + }, + { + "name": "projectPath", + "description": "Xcode project (.xcodeproj) path. Mutually exclusive with workspacePath. Required for most build/test tools when workspacePath is not set." + }, + { + "name": "scheme", + "description": "Xcode scheme. Required by most build/test tools. Use list_schemes to discover available schemes before setting." + }, + { + "name": "simulatorId", + "description": "Simulator UUID for simulator workflows. Preferred over simulatorName when both are provided." + }, + { + "name": "simulatorName", + "description": "Simulator device name for simulator workflows. If simulatorId is also provided, simulatorId is preferred and simulatorName is ignored." + }, + { + "name": "suppressWarnings", + "description": "When true, warning messages are filtered from build output to conserve context" + }, + { + "name": "useLatestOS", + "description": "When true, prefer the latest available OS for simulatorName lookups." + }, + { + "name": "workspacePath", + "description": "Xcode workspace (.xcworkspace) path. Mutually exclusive with projectPath. Required for most build/test tools when projectPath is not set." + } + ] + }, + { + "name": "session-show-defaults", + "description": "Show current session defaults.", + "args": [] + }, + { + "name": "set_sim_appearance", + "description": "Sets the appearance mode (dark/light) of an iOS simulator.", + "args": [ + { + "name": "mode", + "description": "The appearance mode to set (either \"dark\" or \"light\")" + } + ] + }, + { + "name": "set_sim_location", + "description": "Sets a custom GPS location for the simulator.", + "args": [ + { + "name": "latitude", + "description": "The latitude for the custom location." + }, + { + "name": "longitude", + "description": "The longitude for the custom location." + } + ] + }, + { + "name": "show_build_settings", + "description": "Shows xcodebuild build settings.", + "args": [] + }, + { + "name": "sim_statusbar", + "description": "Sets the data network indicator in the iOS simulator status bar. Use \"clear\" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc).", + "args": [ + { + "name": "dataNetwork", + "description": "Data network type to display in status bar. Use \"clear\" to reset all overrides. Valid values: clear, hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc." + } + ] + }, + { + "name": "start_device_log_cap", + "description": "Starts log capture on a connected device.", + "args": [ + { + "name": "bundleId", + "description": "Bundle identifier of the app to launch and capture logs for." + } + ] + }, + { + "name": "start_sim_log_cap", + "description": "Starts capturing logs from a specified simulator. Returns a session ID. Use subsystemFilter to control what logs are captured: 'app' (default), 'all' (everything), 'swiftui' (includes Self._printChanges()), or custom subsystems.", + "args": [ + { + "name": "bundleId", + "description": "Bundle identifier of the app to capture logs for." + }, + { + "name": "captureConsole", + "description": "Whether to capture console output (requires app relaunch)." + }, + { + "name": "subsystemFilter", + "description": "Controls which log subsystems to capture. Options: 'app' (default, only app logs), 'all' (capture all system logs), 'swiftui' (app + SwiftUI logs for Self._printChanges()), or an array of custom subsystem strings." + } + ] + }, + { + "name": "stop_app_device", + "description": "Stops a running app on a connected device.", + "args": [ + { + "name": "processId", + "description": "Process ID (PID) of the app to stop" + } + ] + }, + { + "name": "stop_app_sim", + "description": "Stops an app running in an iOS simulator.", + "args": [ + { + "name": "bundleId", + "description": "Bundle identifier of the app to stop (e.g., 'com.example.MyApp')" + } + ] + }, + { + "name": "stop_device_log_cap", + "description": "Stops an active Apple device log capture session and returns the captured logs.", + "args": [ + { + "name": "logSessionId", + "description": "The session ID returned by start_device_log_cap." + } + ] + }, + { + "name": "stop_mac_app", + "description": "Stops a running macOS application. Can stop by app name or process ID.", + "args": [ + { + "name": "appName", + "description": "Name of the application to stop (e.g., \"Calculator\" or \"MyApp\")" + }, + { + "name": "processId", + "description": "Process ID (PID) of the application to stop" + } + ] + }, + { + "name": "stop_sim_log_cap", + "description": "Stops an active simulator log capture session and returns the captured logs.", + "args": [ + { + "name": "logSessionId", + "description": "The session ID returned by start_sim_log_cap." + } + ] + }, + { + "name": "swift_package_build", + "description": "Builds a Swift Package with swift build", + "args": [ + { + "name": "architectures", + "description": "Target architectures to build for" + }, + { + "name": "configuration", + "description": "Swift package configuration (debug, release)" + }, + { + "name": "packagePath", + "description": "Path to the Swift package root (Required)" + }, + { + "name": "parseAsLibrary", + "description": "Build as library instead of executable" + }, + { + "name": "targetName", + "description": "Optional target to build" + } + ] + }, + { + "name": "swift_package_clean", + "description": "Cleans Swift Package build artifacts and derived data", + "args": [ + { + "name": "packagePath", + "description": "Path to the Swift package root (Required)" + } + ] + }, + { + "name": "swift_package_list", + "description": "Lists currently running Swift Package processes", + "args": [] + }, + { + "name": "swift_package_run", + "description": "Runs an executable target from a Swift Package with swift run", + "args": [ + { + "name": "arguments", + "description": "Arguments to pass to the executable" + }, + { + "name": "background", + "description": "Run in background and return immediately (default: false)" + }, + { + "name": "configuration", + "description": "Build configuration: 'debug' (default) or 'release'" + }, + { + "name": "executableName", + "description": "Name of executable to run (defaults to package name)" + }, + { + "name": "packagePath", + "description": "Path to the Swift package root (Required)" + }, + { + "name": "parseAsLibrary", + "description": "Add -parse-as-library flag for @main support (default: false)" + }, + { + "name": "timeout", + "description": "Timeout in seconds (default: 30, max: 300)" + } + ] + }, + { + "name": "swift_package_stop", + "description": "Stops a running Swift Package executable started with swift_package_run", + "args": [ + { + "name": "pid", + "description": "Process ID (PID) of the running executable" + } + ] + }, + { + "name": "swift_package_test", + "description": "Runs tests for a Swift Package with swift test", + "args": [ + { + "name": "configuration", + "description": "Swift package configuration (debug, release)" + }, + { + "name": "filter", + "description": "Filter tests by name (regex pattern)" + }, + { + "name": "packagePath", + "description": "Path to the Swift package root (Required)" + }, + { + "name": "parallel", + "description": "Run tests in parallel (default: true)" + }, + { + "name": "parseAsLibrary", + "description": "Add -parse-as-library flag for @main support (default: false)" + }, + { + "name": "showCodecov", + "description": "Show code coverage (default: false)" + }, + { + "name": "testProduct", + "description": "Optional specific test product to run" + } + ] + }, + { + "name": "swipe", + "description": "Swipe between points.", + "args": [ + { + "name": "delta", + "description": null + }, + { + "name": "duration", + "description": null + }, + { + "name": "postDelay", + "description": null + }, + { + "name": "preDelay", + "description": null + }, + { + "name": "x1", + "description": null + }, + { + "name": "x2", + "description": null + }, + { + "name": "y1", + "description": null + }, + { + "name": "y2", + "description": null + } + ] + }, + { + "name": "tap", + "description": "Tap coordinate or element.", + "args": [ + { + "name": "id", + "description": null + }, + { + "name": "label", + "description": null + }, + { + "name": "postDelay", + "description": null + }, + { + "name": "preDelay", + "description": null + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "test_device", + "description": "Runs tests on a physical Apple device.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path to derived data directory" + }, + { + "name": "extraArgs", + "description": "Additional arguments to pass to xcodebuild" + }, + { + "name": "platform", + "description": "Target platform (defaults to iOS)" + }, + { + "name": "preferXcodebuild", + "description": "Prefer xcodebuild over faster alternatives" + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "test_macos", + "description": "Runs tests for a macOS target.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path where build products and other derived data will go" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system" + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "test_sim", + "description": "Runs tests on an iOS simulator.", + "args": [ + { + "name": "derivedDataPath", + "description": "Path where build products and other derived data will go" + }, + { + "name": "extraArgs", + "description": "Additional xcodebuild arguments" + }, + { + "name": "preferXcodebuild", + "description": "If true, prefers xcodebuild over the experimental incremental build system, useful for when incremental build system fails." + }, + { + "name": "testRunnerEnv", + "description": "Environment variables to pass to the test runner (TEST_RUNNER_ prefix added automatically)" + } + ] + }, + { + "name": "touch", + "description": "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).", + "args": [ + { + "name": "delay", + "description": null + }, + { + "name": "down", + "description": null + }, + { + "name": "up", + "description": null + }, + { + "name": "x", + "description": null + }, + { + "name": "y", + "description": null + } + ] + }, + { + "name": "type_text", + "description": "Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.", + "args": [ + { + "name": "text", + "description": null + } + ] + } + ] +} \ No newline at end of file From e667a733e12fc053e4cddbfdc01aa50ab9c84f60 Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Fri, 23 Jan 2026 00:36:25 +0000 Subject: [PATCH 3/6] Move redundent tool arguments to session-defaults --- docs/dev/tools_schema_redundancy.md | 65 +++++++-------- .../device/__tests__/build_device.test.ts | 8 +- .../__tests__/get_device_app_path.test.ts | 6 +- .../__tests__/launch_app_device.test.ts | 13 +-- .../device/__tests__/test_device.test.ts | 14 +--- src/mcp/tools/device/build_device.ts | 2 + src/mcp/tools/device/get_device_app_path.ts | 1 + src/mcp/tools/device/launch_app_device.ts | 7 +- src/mcp/tools/device/test_device.ts | 3 + .../__tests__/start_device_log_cap.test.ts | 13 +-- .../__tests__/start_sim_log_cap.test.ts | 57 ++++--------- src/mcp/tools/logging/start_device_log_cap.ts | 7 +- src/mcp/tools/logging/start_sim_log_cap.ts | 6 +- .../tools/macos/__tests__/build_macos.test.ts | 16 ++-- .../macos/__tests__/build_run_macos.test.ts | 18 ++--- .../tools/macos/__tests__/test_macos.test.ts | 12 +-- src/mcp/tools/macos/build_macos.ts | 2 + src/mcp/tools/macos/build_run_macos.ts | 2 + src/mcp/tools/macos/test_macos.ts | 2 + .../session_set_defaults.ts | 21 ++++- .../simulator/__tests__/build_run_sim.test.ts | 10 +-- .../simulator/__tests__/build_sim.test.ts | 10 +-- .../__tests__/launch_app_logs_sim.test.ts | 23 +++--- .../__tests__/launch_app_sim.test.ts | 28 ++----- .../simulator/__tests__/stop_app_sim.test.ts | 17 ++-- .../simulator/__tests__/test_sim.test.ts | 10 +-- src/mcp/tools/simulator/build_run_sim.ts | 2 + src/mcp/tools/simulator/build_sim.ts | 2 + .../tools/simulator/launch_app_logs_sim.ts | 5 +- src/mcp/tools/simulator/launch_app_sim.ts | 2 + src/mcp/tools/simulator/stop_app_sim.ts | 2 + src/mcp/tools/simulator/test_sim.ts | 2 + .../__tests__/swift_package_build.test.ts | 48 ++++++----- .../__tests__/swift_package_run.test.ts | 80 ++++++++++--------- .../__tests__/swift_package_test.test.ts | 63 +++++++++------ .../swift-package/swift_package_build.ts | 30 ++++--- .../tools/swift-package/swift_package_run.ts | 30 ++++--- .../tools/swift-package/swift_package_test.ts | 30 ++++--- .../tools/utilities/__tests__/clean.test.ts | 8 +- src/mcp/tools/utilities/clean.ts | 2 + src/utils/session-store.ts | 4 + 41 files changed, 363 insertions(+), 320 deletions(-) diff --git a/docs/dev/tools_schema_redundancy.md b/docs/dev/tools_schema_redundancy.md index 7c379141..83828acb 100644 --- a/docs/dev/tools_schema_redundancy.md +++ b/docs/dev/tools_schema_redundancy.md @@ -1,17 +1,14 @@ # Tools Schema Session-Default Audit -This document identifies arguments in `tools.compact.json` that should be removed from individual tool schemas because they are typically set once per session. It also provides a concrete checklist of tool-level removals and default additions. +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 -- derivedDataPath: Add to session defaults; remove from build/test/clean tool schemas. -- preferXcodebuild: Add to session defaults; remove from build/test/clean tool schemas. -- configuration (SwiftPM): Add to SwiftPM session defaults; applies to Swift Package build/run/test tools only. -- platform: Add to device session defaults; applies to device-only tools (get_device_app_path, test_device). - -## Optional session defaults (sticky per workflow) - -- bundleId: Consider as a session default for launch/stop/log tools when working on a single app. +- [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 @@ -19,39 +16,39 @@ This document identifies arguments in `tools.compact.json` that should be remove Remove derivedDataPath, preferXcodebuild from: -- build_device -- build_sim -- build_run_sim -- build_macos -- build_run_macos -- test_device -- test_sim -- test_macos -- clean +- [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: -- get_device_app_path -- test_device +- [x] get_device_app_path +- [x] test_device ### Swift Package Manager -Remove configuration (if promoted to session default) from: +Remove configuration from: -- swift_package_build -- swift_package_run -- swift_package_test +- [x] swift_package_build +- [x] swift_package_run +- [x] swift_package_test ### Launch/stop/log (bundleId default) -Remove bundleId (if promoted to session default) from: +Remove bundleId from: -- launch_app_device -- launch_app_sim -- launch_app_logs_sim -- stop_app_sim -- start_device_log_cap -- start_sim_log_cap +- [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) @@ -62,7 +59,5 @@ Remove bundleId (if promoted to session default) from: ## Description updates -When removing arguments from individual schemas, ensure tool descriptions mention: - -- The session default that will be used. -- How to override per call (if overrides remain supported). +- [x] session-set-defaults configuration description clarifies SwiftPM + Xcode usage. +- [x] session-set-defaults platform description clarifies device-only usage. diff --git a/src/mcp/tools/device/__tests__/build_device.test.ts b/src/mcp/tools/device/__tests__/build_device.test.ts index 2b55248a..67bc7e59 100644 --- a/src/mcp/tools/device/__tests__/build_device.test.ts +++ b/src/mcp/tools/device/__tests__/build_device.test.ts @@ -35,13 +35,13 @@ describe('build_device plugin', () => { it('should expose only optional build-tuning fields in public schema', () => { const schema = z.strictObject(buildDevice.schema); expect(schema.safeParse({}).success).toBe(true); - expect( - schema.safeParse({ derivedDataPath: '/path/to/derived-data', extraArgs: [] }).success, - ).toBe(true); + expect(schema.safeParse({ extraArgs: [] }).success).toBe(true); + expect(schema.safeParse({ derivedDataPath: '/path/to/derived-data' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); expect(schema.safeParse({ projectPath: '/path/to/MyProject.xcodeproj' }).success).toBe(false); const schemaKeys = Object.keys(buildDevice.schema).sort(); - expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild']); + expect(schemaKeys).toEqual(['extraArgs']); }); }); diff --git a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts index c394f800..ee83119b 100644 --- a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts +++ b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts @@ -33,14 +33,14 @@ describe('get_device_app_path plugin', () => { expect(typeof getDeviceAppPath.handler).toBe('function'); }); - it('should expose only platform in public schema', () => { + it('should expose empty public schema', () => { const schema = z.strictObject(getDeviceAppPath.schema); expect(schema.safeParse({}).success).toBe(true); - expect(schema.safeParse({ platform: 'iOS' }).success).toBe(true); + expect(schema.safeParse({ platform: 'iOS' }).success).toBe(false); expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false); const schemaKeys = Object.keys(getDeviceAppPath.schema).sort(); - expect(schemaKeys).toEqual(['platform']); + expect(schemaKeys).toEqual([]); }); }); diff --git a/src/mcp/tools/device/__tests__/launch_app_device.test.ts b/src/mcp/tools/device/__tests__/launch_app_device.test.ts index bbb52b4c..b87661da 100644 --- a/src/mcp/tools/device/__tests__/launch_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/launch_app_device.test.ts @@ -36,9 +36,9 @@ describe('launch_app_device plugin (device-shared)', () => { it('should validate schema with valid inputs', () => { const schema = z.strictObject(launchAppDevice.schema); - expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); - expect(schema.safeParse({}).success).toBe(false); - expect(Object.keys(launchAppDevice.schema)).toEqual(['bundleId']); + expect(schema.safeParse({}).success).toBe(true); + expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(false); + expect(Object.keys(launchAppDevice.schema)).toEqual([]); }); it('should validate schema with invalid inputs', () => { @@ -49,11 +49,12 @@ describe('launch_app_device plugin (device-shared)', () => { }); describe('Handler Requirements', () => { - it('should require deviceId when not provided', async () => { - const result = await launchAppDevice.handler({ bundleId: 'com.example.app' }); + it('should require deviceId and bundleId when not provided', async () => { + const result = await launchAppDevice.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('deviceId is required'); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('Provide deviceId and bundleId'); }); }); diff --git a/src/mcp/tools/device/__tests__/test_device.test.ts b/src/mcp/tools/device/__tests__/test_device.test.ts index a07486e0..6468dcf6 100644 --- a/src/mcp/tools/device/__tests__/test_device.test.ts +++ b/src/mcp/tools/device/__tests__/test_device.test.ts @@ -37,24 +37,18 @@ describe('test_device plugin', () => { const schema = z.strictObject(testDevice.schema); expect( schema.safeParse({ - derivedDataPath: '/path/to/derived-data', extraArgs: ['--arg1'], - preferXcodebuild: true, - platform: 'iOS', testRunnerEnv: { FOO: 'bar' }, }).success, ).toBe(true); expect(schema.safeParse({}).success).toBe(true); + expect(schema.safeParse({ derivedDataPath: '/path/to/derived-data' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); + expect(schema.safeParse({ platform: 'iOS' }).success).toBe(false); expect(schema.safeParse({ projectPath: '/path/to/project.xcodeproj' }).success).toBe(false); const schemaKeys = Object.keys(testDevice.schema).sort(); - expect(schemaKeys).toEqual([ - 'derivedDataPath', - 'extraArgs', - 'platform', - 'preferXcodebuild', - 'testRunnerEnv', - ]); + expect(schemaKeys).toEqual(['extraArgs', 'testRunnerEnv']); }); it('should validate XOR between projectPath and workspacePath', async () => { diff --git a/src/mcp/tools/device/build_device.ts b/src/mcp/tools/device/build_device.ts index 5ba4f399..8ad40868 100644 --- a/src/mcp/tools/device/build_device.ts +++ b/src/mcp/tools/device/build_device.ts @@ -45,6 +45,8 @@ const publicSchemaObject = baseSchemaObject.omit({ workspacePath: true, scheme: true, configuration: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); /** diff --git a/src/mcp/tools/device/get_device_app_path.ts b/src/mcp/tools/device/get_device_app_path.ts index 7968048c..387bbfb2 100644 --- a/src/mcp/tools/device/get_device_app_path.ts +++ b/src/mcp/tools/device/get_device_app_path.ts @@ -49,6 +49,7 @@ const publicSchemaObject = baseSchemaObject.omit({ workspacePath: true, scheme: true, configuration: true, + platform: true, } as const); export async function get_device_app_pathLogic( diff --git a/src/mcp/tools/device/launch_app_device.ts b/src/mcp/tools/device/launch_app_device.ts index f560ae17..f9dbffdb 100644 --- a/src/mcp/tools/device/launch_app_device.ts +++ b/src/mcp/tools/device/launch_app_device.ts @@ -34,7 +34,10 @@ const launchAppDeviceSchema = z.object({ bundleId: z.string(), }); -const publicSchemaObject = launchAppDeviceSchema.omit({ deviceId: true } as const); +const publicSchemaObject = launchAppDeviceSchema.omit({ + deviceId: true, + bundleId: true, +} as const); // Use z.infer for type safety type LaunchAppDeviceParams = z.infer; @@ -160,6 +163,6 @@ export default { logicFunction: (params, executor) => launch_app_deviceLogic(params, executor, getDefaultFileSystemExecutor()), getExecutor: getDefaultCommandExecutor, - requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], + requirements: [{ allOf: ['deviceId', 'bundleId'], message: 'Provide deviceId and bundleId' }], }), }; diff --git a/src/mcp/tools/device/test_device.ts b/src/mcp/tools/device/test_device.ts index 005d40a8..3186a11e 100644 --- a/src/mcp/tools/device/test_device.ts +++ b/src/mcp/tools/device/test_device.ts @@ -65,6 +65,9 @@ const publicSchemaObject = baseSchemaObject.omit({ scheme: true, deviceId: true, configuration: true, + derivedDataPath: true, + preferXcodebuild: true, + platform: true, } as const); /** 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 959cdfeb..d110c041 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 @@ -70,12 +70,12 @@ describe('start_device_log_cap plugin', () => { it('should have correct schema structure', () => { // Schema should be a plain object for MCP protocol compliance expect(typeof plugin.schema).toBe('object'); - expect(Object.keys(plugin.schema)).toEqual(['bundleId']); + expect(Object.keys(plugin.schema)).toEqual([]); // Validate that schema fields are Zod types that can be used for validation const schema = z.strictObject(plugin.schema); - expect(schema.safeParse({ bundleId: 'com.test.app' }).success).toBe(true); - expect(schema.safeParse({}).success).toBe(false); + expect(schema.safeParse({ bundleId: 'com.test.app' }).success).toBe(false); + expect(schema.safeParse({}).success).toBe(true); }); it('should have handler as a function', () => { @@ -84,11 +84,12 @@ describe('start_device_log_cap plugin', () => { }); describe('Handler Requirements', () => { - it('should require deviceId when not provided', async () => { - const result = await plugin.handler({ bundleId: 'com.example.MyApp' }); + it('should require deviceId and bundleId when not provided', async () => { + const result = await plugin.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('deviceId is required'); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('Provide deviceId and bundleId'); }); }); 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 95e2c90a..073b42d6 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 @@ -31,63 +31,34 @@ describe('start_sim_log_cap plugin', () => { it('should validate schema with valid parameters', () => { const schema = z.object(plugin.schema); - expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); - expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: true }).success).toBe( - true, - ); - expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: false }).success).toBe( - true, - ); + expect(schema.safeParse({}).success).toBe(true); + expect(schema.safeParse({ captureConsole: true }).success).toBe(true); + expect(schema.safeParse({ captureConsole: false }).success).toBe(true); }); it('should validate schema with subsystemFilter parameter', () => { const schema = z.object(plugin.schema); // Valid enum values - expect( - schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: 'app' }).success, - ).toBe(true); - expect( - schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: 'all' }).success, - ).toBe(true); - expect( - schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: 'swiftui' }).success, - ).toBe(true); + expect(schema.safeParse({ subsystemFilter: 'app' }).success).toBe(true); + expect(schema.safeParse({ subsystemFilter: 'all' }).success).toBe(true); + expect(schema.safeParse({ subsystemFilter: 'swiftui' }).success).toBe(true); // Valid array of subsystems + expect(schema.safeParse({ subsystemFilter: ['com.apple.UIKit'] }).success).toBe(true); expect( - schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: ['com.apple.UIKit'] }) - .success, - ).toBe(true); - expect( - schema.safeParse({ - bundleId: 'com.example.app', - subsystemFilter: ['com.apple.UIKit', 'com.apple.CoreData'], - }).success, + schema.safeParse({ subsystemFilter: ['com.apple.UIKit', 'com.apple.CoreData'] }).success, ).toBe(true); // Invalid values - expect(schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: [] }).success).toBe( - false, - ); - expect( - schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: 'invalid' }).success, - ).toBe(false); - expect(schema.safeParse({ bundleId: 'com.example.app', subsystemFilter: 123 }).success).toBe( - false, - ); + expect(schema.safeParse({ subsystemFilter: [] }).success).toBe(false); + expect(schema.safeParse({ subsystemFilter: 'invalid' }).success).toBe(false); + expect(schema.safeParse({ subsystemFilter: 123 }).success).toBe(false); }); it('should reject invalid schema parameters', () => { const schema = z.object(plugin.schema); - expect(schema.safeParse({ bundleId: null }).success).toBe(false); - expect(schema.safeParse({ captureConsole: true }).success).toBe(false); - expect(schema.safeParse({}).success).toBe(false); - expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 'yes' }).success).toBe( - false, - ); - expect(schema.safeParse({ bundleId: 'com.example.app', captureConsole: 123 }).success).toBe( - false, - ); + expect(schema.safeParse({ captureConsole: 'yes' }).success).toBe(false); + expect(schema.safeParse({ captureConsole: 123 }).success).toBe(false); - const withSimId = schema.safeParse({ simulatorId: 'test-uuid', bundleId: 'com.example.app' }); + const withSimId = schema.safeParse({ simulatorId: 'test-uuid' }); expect(withSimId.success).toBe(true); expect('simulatorId' in (withSimId.data as any)).toBe(false); }); diff --git a/src/mcp/tools/logging/start_device_log_cap.ts b/src/mcp/tools/logging/start_device_log_cap.ts index 8c7c1f37..694e9e40 100644 --- a/src/mcp/tools/logging/start_device_log_cap.ts +++ b/src/mcp/tools/logging/start_device_log_cap.ts @@ -617,7 +617,10 @@ const startDeviceLogCapSchema = z.object({ bundleId: z.string(), }); -const publicSchemaObject = startDeviceLogCapSchema.omit({ deviceId: true } as const); +const publicSchemaObject = startDeviceLogCapSchema.omit({ + deviceId: true, + bundleId: true, +} as const); // Use z.infer for type safety type StartDeviceLogCapParams = z.infer; @@ -683,6 +686,6 @@ export default { >, logicFunction: start_device_log_capLogic, getExecutor: getDefaultCommandExecutor, - requirements: [{ allOf: ['deviceId'], message: 'deviceId is required' }], + requirements: [{ allOf: ['deviceId', 'bundleId'], message: 'Provide deviceId and bundleId' }], }), }; diff --git a/src/mcp/tools/logging/start_sim_log_cap.ts b/src/mcp/tools/logging/start_sim_log_cap.ts index 569d9fd2..5eae2eb7 100644 --- a/src/mcp/tools/logging/start_sim_log_cap.ts +++ b/src/mcp/tools/logging/start_sim_log_cap.ts @@ -80,7 +80,7 @@ export async function start_sim_log_capLogic( } const publicSchemaObject = z.strictObject( - startSimLogCapSchema.omit({ simulatorId: true } as const).shape, + startSimLogCapSchema.omit({ simulatorId: true, bundleId: true } as const).shape, ); export default { @@ -98,6 +98,8 @@ export default { internalSchema: startSimLogCapSchema as unknown as z.ZodType, logicFunction: start_sim_log_capLogic, getExecutor: getDefaultCommandExecutor, - requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + requirements: [ + { allOf: ['simulatorId', 'bundleId'], message: 'Provide simulatorId and bundleId' }, + ], }), }; diff --git a/src/mcp/tools/macos/__tests__/build_macos.test.ts b/src/mcp/tools/macos/__tests__/build_macos.test.ts index 83820a95..2a343aea 100644 --- a/src/mcp/tools/macos/__tests__/build_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_macos.test.ts @@ -30,23 +30,17 @@ describe('build_macos plugin', () => { }); it('should validate schema correctly', () => { - const schema = z.object(buildMacOS.schema); + const schema = z.strictObject(buildMacOS.schema); expect(schema.safeParse({}).success).toBe(true); - expect( - schema.safeParse({ - derivedDataPath: '/path/to/derived-data', - extraArgs: ['--arg1', '--arg2'], - preferXcodebuild: true, - }).success, - ).toBe(true); + expect(schema.safeParse({ extraArgs: ['--arg1', '--arg2'] }).success).toBe(true); - expect(schema.safeParse({ derivedDataPath: 42 }).success).toBe(false); + expect(schema.safeParse({ derivedDataPath: '/path/to/derived-data' }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false); - expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); const schemaKeys = Object.keys(buildMacOS.schema).sort(); - expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort()); + expect(schemaKeys).toEqual(['extraArgs']); }); }); diff --git a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts index bad1a519..cc384812 100644 --- a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts @@ -23,23 +23,17 @@ describe('build_run_macos', () => { }); it('should expose only non-session fields in schema', () => { - const schema = z.object(tool.schema); + const schema = z.strictObject(tool.schema); expect(schema.safeParse({}).success).toBe(true); - expect( - schema.safeParse({ - derivedDataPath: '/tmp/derived', - extraArgs: ['--verbose'], - preferXcodebuild: true, - }).success, - ).toBe(true); - - expect(schema.safeParse({ derivedDataPath: 1 }).success).toBe(false); + expect(schema.safeParse({ extraArgs: ['--verbose'] }).success).toBe(true); + + expect(schema.safeParse({ derivedDataPath: '/tmp/derived' }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--ok', 2] }).success).toBe(false); - expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); const schemaKeys = Object.keys(tool.schema).sort(); - expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort()); + expect(schemaKeys).toEqual(['extraArgs']); }); }); diff --git a/src/mcp/tools/macos/__tests__/test_macos.test.ts b/src/mcp/tools/macos/__tests__/test_macos.test.ts index ac305835..a0cdfe7f 100644 --- a/src/mcp/tools/macos/__tests__/test_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/test_macos.test.ts @@ -42,27 +42,23 @@ describe('test_macos plugin (unified)', () => { }); it('should validate schema correctly', () => { - const schema = z.object(testMacos.schema); + const schema = z.strictObject(testMacos.schema); expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ - derivedDataPath: '/path/to/derived-data', extraArgs: ['--arg1', '--arg2'], - preferXcodebuild: true, testRunnerEnv: { FOO: 'BAR' }, }).success, ).toBe(true); - expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false); + expect(schema.safeParse({ derivedDataPath: '/path/to/derived-data' }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--ok', 1] }).success).toBe(false); - expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); expect(schema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false); const schemaKeys = Object.keys(testMacos.schema).sort(); - expect(schemaKeys).toEqual( - ['derivedDataPath', 'extraArgs', 'preferXcodebuild', 'testRunnerEnv'].sort(), - ); + expect(schemaKeys).toEqual(['extraArgs', 'testRunnerEnv'].sort()); }); }); diff --git a/src/mcp/tools/macos/build_macos.ts b/src/mcp/tools/macos/build_macos.ts index 810385b8..219e4a74 100644 --- a/src/mcp/tools/macos/build_macos.ts +++ b/src/mcp/tools/macos/build_macos.ts @@ -48,6 +48,8 @@ const publicSchemaObject = baseSchemaObject.omit({ scheme: true, configuration: true, arch: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); const buildMacOSSchema = z.preprocess( diff --git a/src/mcp/tools/macos/build_run_macos.ts b/src/mcp/tools/macos/build_run_macos.ts index 28a806a7..91f0a2b3 100644 --- a/src/mcp/tools/macos/build_run_macos.ts +++ b/src/mcp/tools/macos/build_run_macos.ts @@ -39,6 +39,8 @@ const publicSchemaObject = baseSchemaObject.omit({ scheme: true, configuration: true, arch: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); const buildRunMacOSSchema = z.preprocess( diff --git a/src/mcp/tools/macos/test_macos.ts b/src/mcp/tools/macos/test_macos.ts index 2b02b1b5..f82357e7 100644 --- a/src/mcp/tools/macos/test_macos.ts +++ b/src/mcp/tools/macos/test_macos.ts @@ -49,6 +49,8 @@ const publicSchemaObject = baseSchemaObject.omit({ workspacePath: true, scheme: true, configuration: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); const testMacosSchema = z.preprocess( diff --git a/src/mcp/tools/session-management/session_set_defaults.ts b/src/mcp/tools/session-management/session_set_defaults.ts index 13787fb8..8cd05f79 100644 --- a/src/mcp/tools/session-management/session_set_defaults.ts +++ b/src/mcp/tools/session-management/session_set_defaults.ts @@ -8,13 +8,32 @@ const baseSchema = z.object({ projectPath: z.string().optional().describe('xcodeproj path (xor workspacePath)'), workspacePath: z.string().optional().describe('xcworkspace path (xor projectPath)'), scheme: z.string().optional(), - configuration: z.string().optional().describe("e.g. 'Debug' or 'Release'."), + configuration: z + .string() + .optional() + .describe("Build configuration for Xcode and SwiftPM tools (e.g. 'Debug' or 'Release')."), simulatorName: z.string().optional(), simulatorId: z.string().optional(), deviceId: z.string().optional(), useLatestOS: z.boolean().optional(), arch: z.enum(['arm64', 'x86_64']).optional(), suppressWarnings: z.boolean().optional(), + derivedDataPath: z + .string() + .optional() + .describe('Default DerivedData path for Xcode build/test/clean tools.'), + preferXcodebuild: z + .boolean() + .optional() + .describe('Prefer xcodebuild over incremental builds for Xcode build/test/clean tools.'), + platform: z + .string() + .optional() + .describe('Default device platform for device tools (e.g. iOS, watchOS).'), + bundleId: z + .string() + .optional() + .describe('Default bundle ID for launch/stop/log tools when working on a single app.'), }); const schemaObj = baseSchema; 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 d0d51f53..5af66ccd 100644 --- a/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_run_sim.test.ts @@ -32,24 +32,22 @@ describe('build_run_sim tool', () => { }); it('should expose only non-session fields in public schema', () => { - const schema = z.object(buildRunSim.schema); + const schema = z.strictObject(buildRunSim.schema); expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ - derivedDataPath: '/path/to/derived', extraArgs: ['--verbose'], - preferXcodebuild: false, }).success, ).toBe(true); - expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false); + expect(schema.safeParse({ derivedDataPath: '/path/to/derived' }).success).toBe(false); expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false); - expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: false }).success).toBe(false); const schemaKeys = Object.keys(buildRunSim.schema).sort(); - expect(schemaKeys).toEqual(['derivedDataPath', 'extraArgs', 'preferXcodebuild'].sort()); + expect(schemaKeys).toEqual(['extraArgs']); expect(schemaKeys).not.toContain('scheme'); expect(schemaKeys).not.toContain('simulatorName'); expect(schemaKeys).not.toContain('projectPath'); diff --git a/src/mcp/tools/simulator/__tests__/build_sim.test.ts b/src/mcp/tools/simulator/__tests__/build_sim.test.ts index 0ee980a1..4773bfde 100644 --- a/src/mcp/tools/simulator/__tests__/build_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/build_sim.test.ts @@ -29,7 +29,7 @@ describe('build_sim tool', () => { }); it('should have correct public schema (only non-session fields)', () => { - const schema = z.object(buildSim.schema); + const schema = z.strictObject(buildSim.schema); // Public schema should allow empty input expect(schema.safeParse({}).success).toBe(true); @@ -37,16 +37,14 @@ describe('build_sim tool', () => { // Valid public inputs expect( schema.safeParse({ - derivedDataPath: '/path/to/derived', extraArgs: ['--verbose'], - preferXcodebuild: false, }).success, ).toBe(true); - // Invalid types on public inputs - expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false); + // Invalid types or unknown fields on public inputs + expect(schema.safeParse({ derivedDataPath: '/path/to/derived' }).success).toBe(false); expect(schema.safeParse({ extraArgs: [123] }).success).toBe(false); - expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: false }).success).toBe(false); }); }); 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 d47998c6..785170d9 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 @@ -26,18 +26,15 @@ describe('launch_app_logs_sim tool', () => { it('should expose only non-session fields in public schema', () => { const schema = z.object(launchAppLogsSim.schema); + expect(schema.safeParse({}).success).toBe(true); + expect(schema.safeParse({ args: ['--debug'] }).success).toBe(true); expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); - expect(schema.safeParse({ bundleId: 'com.example.app', args: ['--debug'] }).success).toBe( - true, - ); - expect(schema.safeParse({}).success).toBe(false); - expect(schema.safeParse({ bundleId: 42 }).success).toBe(false); + expect(schema.safeParse({ bundleId: 42 }).success).toBe(true); - expect(Object.keys(launchAppLogsSim.schema).sort()).toEqual(['args', 'bundleId'].sort()); + expect(Object.keys(launchAppLogsSim.schema).sort()).toEqual(['args']); const withSimId = schema.safeParse({ simulatorId: 'abc123', - bundleId: 'com.example.app', }); expect(withSimId.success).toBe(true); expect('simulatorId' in (withSimId.data as Record)).toBe(false); @@ -46,24 +43,22 @@ describe('launch_app_logs_sim tool', () => { describe('Handler Requirements', () => { it('should require simulatorId when not provided', async () => { - const result = await launchAppLogsSim.handler({ bundleId: 'com.example.testapp' }); + const result = await launchAppLogsSim.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); - expect(result.content[0].text).toContain('simulatorId is required'); + expect(result.content[0].text).toContain('Provide simulatorId and bundleId'); expect(result.content[0].text).toContain('session-set-defaults'); }); - it('should validate bundleId when simulatorId default exists', async () => { + it('should require bundleId when simulatorId default exists', async () => { sessionStore.setDefaults({ simulatorId: 'SIM-UUID' }); const result = await launchAppLogsSim.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Parameter validation failed'); - expect(result.content[0].text).toContain( - 'bundleId: Invalid input: expected string, received undefined', - ); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('Provide simulatorId and bundleId'); }); }); 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 bfdb3fda..5aa297df 100644 --- a/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/launch_app_sim.test.ts @@ -16,36 +16,26 @@ describe('launch_app_sim tool', () => { }); it('should expose only non-session fields in public schema', () => { - const schema = z.object(launchAppSim.schema); + const schema = z.strictObject(launchAppSim.schema); - expect( - schema.safeParse({ - bundleId: 'com.example.testapp', - }).success, - ).toBe(true); + expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ - bundleId: 'com.example.testapp', args: ['--debug'], }).success, ).toBe(true); - expect(schema.safeParse({}).success).toBe(false); + expect(schema.safeParse({ bundleId: 'com.example.testapp' }).success).toBe(false); expect(schema.safeParse({ bundleId: 123 }).success).toBe(false); - expect(schema.safeParse({ args: ['--debug'] }).success).toBe(false); - expect(Object.keys(launchAppSim.schema).sort()).toEqual(['args', 'bundleId'].sort()); + expect(Object.keys(launchAppSim.schema).sort()).toEqual(['args']); const withSimDefaults = schema.safeParse({ simulatorId: 'sim-default', simulatorName: 'iPhone 16', - bundleId: 'com.example.testapp', }); - expect(withSimDefaults.success).toBe(true); - const parsed = withSimDefaults.data as Record; - expect(parsed.simulatorId).toBeUndefined(); - expect(parsed.simulatorName).toBeUndefined(); + expect(withSimDefaults.success).toBe(false); }); }); @@ -59,16 +49,14 @@ describe('launch_app_sim tool', () => { expect(result.content[0].text).toContain('session-set-defaults'); }); - it('should validate bundleId when simulatorId default exists', async () => { + it('should require bundleId when simulatorId default exists', async () => { sessionStore.setDefaults({ simulatorId: 'SIM-UUID' }); const result = await launchAppSim.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Parameter validation failed'); - expect(result.content[0].text).toContain( - 'bundleId: Invalid input: expected string, received undefined', - ); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('bundleId is required'); }); it('should reject when both simulatorId and simulatorName provided explicitly', async () => { 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 4c0cb9cc..362eccb5 100644 --- a/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/stop_app_sim.test.ts @@ -19,18 +19,17 @@ describe('stop_app_sim tool', () => { expect(plugin.description).toBe('Stop sim app.'); }); - it('should expose public schema with only bundleId', () => { + it('should expose empty public schema', () => { const schema = z.object(plugin.schema); + expect(schema.safeParse({}).success).toBe(true); expect(schema.safeParse({ bundleId: 'com.example.app' }).success).toBe(true); - expect(schema.safeParse({}).success).toBe(false); - expect(schema.safeParse({ bundleId: 42 }).success).toBe(false); - expect(Object.keys(plugin.schema)).toEqual(['bundleId']); + expect(schema.safeParse({ bundleId: 42 }).success).toBe(true); + expect(Object.keys(plugin.schema)).toEqual([]); const withSessionDefaults = schema.safeParse({ simulatorId: 'SIM-UUID', simulatorName: 'iPhone 16', - bundleId: 'com.example.app', }); expect(withSessionDefaults.success).toBe(true); const parsed = withSessionDefaults.data as Record; @@ -49,16 +48,14 @@ describe('stop_app_sim tool', () => { expect(result.content[0].text).toContain('session-set-defaults'); }); - it('should validate bundleId when simulatorId default exists', async () => { + it('should require bundleId when simulatorId default exists', async () => { sessionStore.setDefaults({ simulatorId: 'SIM-UUID' }); const result = await plugin.handler({}); expect(result.isError).toBe(true); - expect(result.content[0].text).toContain('Parameter validation failed'); - expect(result.content[0].text).toContain( - 'bundleId: Invalid input: expected string, received undefined', - ); + expect(result.content[0].text).toContain('Missing required session defaults'); + expect(result.content[0].text).toContain('bundleId is required'); }); it('should reject mutually exclusive simulator parameters', async () => { diff --git a/src/mcp/tools/simulator/__tests__/test_sim.test.ts b/src/mcp/tools/simulator/__tests__/test_sim.test.ts index 69be7100..3769d918 100644 --- a/src/mcp/tools/simulator/__tests__/test_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/test_sim.test.ts @@ -27,27 +27,23 @@ describe('test_sim tool', () => { }); it('should expose only non-session fields in public schema', () => { - const schema = z.object(testSim.schema); + const schema = z.strictObject(testSim.schema); expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ - derivedDataPath: '/tmp/derived', extraArgs: ['--quiet'], - preferXcodebuild: true, testRunnerEnv: { FOO: 'BAR' }, }).success, ).toBe(true); expect(schema.safeParse({ derivedDataPath: 123 }).success).toBe(false); expect(schema.safeParse({ extraArgs: ['--ok', 42] }).success).toBe(false); - expect(schema.safeParse({ preferXcodebuild: 'yes' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); expect(schema.safeParse({ testRunnerEnv: { FOO: 123 } }).success).toBe(false); const schemaKeys = Object.keys(testSim.schema).sort(); - expect(schemaKeys).toEqual( - ['derivedDataPath', 'extraArgs', 'preferXcodebuild', 'testRunnerEnv'].sort(), - ); + expect(schemaKeys).toEqual(['extraArgs', 'testRunnerEnv'].sort()); }); }); diff --git a/src/mcp/tools/simulator/build_run_sim.ts b/src/mcp/tools/simulator/build_run_sim.ts index bac516fb..04ecf76b 100644 --- a/src/mcp/tools/simulator/build_run_sim.ts +++ b/src/mcp/tools/simulator/build_run_sim.ts @@ -494,6 +494,8 @@ const publicSchemaObject = baseSchemaObject.omit({ simulatorId: true, simulatorName: true, useLatestOS: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); export default { diff --git a/src/mcp/tools/simulator/build_sim.ts b/src/mcp/tools/simulator/build_sim.ts index d707ced2..5a8d75e2 100644 --- a/src/mcp/tools/simulator/build_sim.ts +++ b/src/mcp/tools/simulator/build_sim.ts @@ -141,6 +141,8 @@ const publicSchemaObject = baseSchemaObject.omit({ simulatorId: true, simulatorName: true, useLatestOS: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); export default { diff --git a/src/mcp/tools/simulator/launch_app_logs_sim.ts b/src/mcp/tools/simulator/launch_app_logs_sim.ts index 363b6f72..0f16cef7 100644 --- a/src/mcp/tools/simulator/launch_app_logs_sim.ts +++ b/src/mcp/tools/simulator/launch_app_logs_sim.ts @@ -30,6 +30,7 @@ type LaunchAppLogsSimParams = z.infer; const publicSchemaObject = z.strictObject( launchAppLogsSimSchemaObject.omit({ simulatorId: true, + bundleId: true, } as const).shape, ); @@ -83,6 +84,8 @@ export default { >, logicFunction: launch_app_logs_simLogic, getExecutor: getDefaultCommandExecutor, - requirements: [{ allOf: ['simulatorId'], message: 'simulatorId is required' }], + requirements: [ + { allOf: ['simulatorId', 'bundleId'], message: 'Provide simulatorId and bundleId' }, + ], }), }; diff --git a/src/mcp/tools/simulator/launch_app_sim.ts b/src/mcp/tools/simulator/launch_app_sim.ts index fe9a0952..2caa0730 100644 --- a/src/mcp/tools/simulator/launch_app_sim.ts +++ b/src/mcp/tools/simulator/launch_app_sim.ts @@ -196,6 +196,7 @@ const publicSchemaObject = z.strictObject( baseSchemaObject.omit({ simulatorId: true, simulatorName: true, + bundleId: true, } as const).shape, ); @@ -216,6 +217,7 @@ export default { getExecutor: getDefaultCommandExecutor, requirements: [ { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, + { allOf: ['bundleId'], message: 'bundleId is required' }, ], exclusivePairs: [['simulatorId', 'simulatorName']], }), diff --git a/src/mcp/tools/simulator/stop_app_sim.ts b/src/mcp/tools/simulator/stop_app_sim.ts index 2f5efc44..2e1d4970 100644 --- a/src/mcp/tools/simulator/stop_app_sim.ts +++ b/src/mcp/tools/simulator/stop_app_sim.ts @@ -152,6 +152,7 @@ const publicSchemaObject = z.strictObject( baseSchemaObject.omit({ simulatorId: true, simulatorName: true, + bundleId: true, } as const).shape, ); @@ -172,6 +173,7 @@ export default { getExecutor: getDefaultCommandExecutor, requirements: [ { oneOf: ['simulatorId', 'simulatorName'], message: 'Provide simulatorId or simulatorName' }, + { allOf: ['bundleId'], message: 'bundleId is required' }, ], exclusivePairs: [['simulatorId', 'simulatorName']], }), diff --git a/src/mcp/tools/simulator/test_sim.ts b/src/mcp/tools/simulator/test_sim.ts index d1dfb609..c34bffef 100644 --- a/src/mcp/tools/simulator/test_sim.ts +++ b/src/mcp/tools/simulator/test_sim.ts @@ -118,6 +118,8 @@ const publicSchemaObject = baseSchemaObject.omit({ simulatorName: true, configuration: true, useLatestOS: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); export default { diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts index f0a746a8..b2a04a60 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_build.test.ts @@ -5,6 +5,7 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; +import * as z from 'zod'; import { createMockExecutor, createMockFileSystemExecutor, @@ -29,26 +30,33 @@ describe('swift_package_build plugin', () => { }); it('should validate schema correctly', () => { - // Test required fields - expect(swiftPackageBuild.schema.packagePath.safeParse('/test/package').success).toBe(true); - expect(swiftPackageBuild.schema.packagePath.safeParse('').success).toBe(true); - - // Test optional fields - expect(swiftPackageBuild.schema.targetName.safeParse('MyTarget').success).toBe(true); - expect(swiftPackageBuild.schema.targetName.safeParse(undefined).success).toBe(true); - expect(swiftPackageBuild.schema.configuration.safeParse('debug').success).toBe(true); - expect(swiftPackageBuild.schema.configuration.safeParse('release').success).toBe(true); - expect(swiftPackageBuild.schema.configuration.safeParse(undefined).success).toBe(true); - expect(swiftPackageBuild.schema.architectures.safeParse(['arm64']).success).toBe(true); - expect(swiftPackageBuild.schema.architectures.safeParse(undefined).success).toBe(true); - expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(true).success).toBe(true); - expect(swiftPackageBuild.schema.parseAsLibrary.safeParse(undefined).success).toBe(true); - - // Test invalid inputs - expect(swiftPackageBuild.schema.packagePath.safeParse(null).success).toBe(false); - expect(swiftPackageBuild.schema.configuration.safeParse('invalid').success).toBe(false); - expect(swiftPackageBuild.schema.architectures.safeParse('not-array').success).toBe(false); - expect(swiftPackageBuild.schema.parseAsLibrary.safeParse('yes').success).toBe(false); + const schema = z.strictObject(swiftPackageBuild.schema); + + expect(schema.safeParse({ packagePath: '/test/package' }).success).toBe(true); + expect(schema.safeParse({ packagePath: '' }).success).toBe(true); + + expect( + schema.safeParse({ + packagePath: '/test/package', + targetName: 'MyTarget', + architectures: ['arm64'], + parseAsLibrary: true, + }).success, + ).toBe(true); + + expect(schema.safeParse({ packagePath: null }).success).toBe(false); + expect( + schema.safeParse({ packagePath: '/test/package', configuration: 'release' }).success, + ).toBe(false); + expect( + schema.safeParse({ packagePath: '/test/package', architectures: 'not-array' }).success, + ).toBe(false); + expect( + schema.safeParse({ packagePath: '/test/package', parseAsLibrary: 'yes' }).success, + ).toBe(false); + + const schemaKeys = Object.keys(swiftPackageBuild.schema).sort(); + expect(schemaKeys).toEqual(['architectures', 'packagePath', 'parseAsLibrary', 'targetName']); }); }); diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts index ca823f9b..663c6ac6 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_run.test.ts @@ -29,42 +29,50 @@ describe('swift_package_run plugin', () => { }); it('should validate schema correctly', () => { - // Test packagePath (required string) - expect(swiftPackageRun.schema.packagePath.safeParse('valid/path').success).toBe(true); - expect(swiftPackageRun.schema.packagePath.safeParse(null).success).toBe(false); - - // Test executableName (optional string) - expect(swiftPackageRun.schema.executableName.safeParse('MyExecutable').success).toBe(true); - expect(swiftPackageRun.schema.executableName.safeParse(undefined).success).toBe(true); - expect(swiftPackageRun.schema.executableName.safeParse(123).success).toBe(false); - - // Test arguments (optional array of strings) - expect(swiftPackageRun.schema.arguments.safeParse(['arg1', 'arg2']).success).toBe(true); - expect(swiftPackageRun.schema.arguments.safeParse(undefined).success).toBe(true); - expect(swiftPackageRun.schema.arguments.safeParse(['arg1', 123]).success).toBe(false); - - // Test configuration (optional enum) - expect(swiftPackageRun.schema.configuration.safeParse('debug').success).toBe(true); - expect(swiftPackageRun.schema.configuration.safeParse('release').success).toBe(true); - expect(swiftPackageRun.schema.configuration.safeParse(undefined).success).toBe(true); - expect(swiftPackageRun.schema.configuration.safeParse('invalid').success).toBe(false); - - // Test timeout (optional number) - expect(swiftPackageRun.schema.timeout.safeParse(30).success).toBe(true); - expect(swiftPackageRun.schema.timeout.safeParse(undefined).success).toBe(true); - expect(swiftPackageRun.schema.timeout.safeParse('30').success).toBe(false); - - // Test background (optional boolean) - expect(swiftPackageRun.schema.background.safeParse(true).success).toBe(true); - expect(swiftPackageRun.schema.background.safeParse(false).success).toBe(true); - expect(swiftPackageRun.schema.background.safeParse(undefined).success).toBe(true); - expect(swiftPackageRun.schema.background.safeParse('true').success).toBe(false); - - // Test parseAsLibrary (optional boolean) - expect(swiftPackageRun.schema.parseAsLibrary.safeParse(true).success).toBe(true); - expect(swiftPackageRun.schema.parseAsLibrary.safeParse(false).success).toBe(true); - expect(swiftPackageRun.schema.parseAsLibrary.safeParse(undefined).success).toBe(true); - expect(swiftPackageRun.schema.parseAsLibrary.safeParse('true').success).toBe(false); + const schema = z.strictObject(swiftPackageRun.schema); + + expect(schema.safeParse({ packagePath: 'valid/path' }).success).toBe(true); + expect(schema.safeParse({ packagePath: null }).success).toBe(false); + + expect( + schema.safeParse({ + packagePath: 'valid/path', + executableName: 'MyExecutable', + arguments: ['arg1', 'arg2'], + timeout: 30, + background: true, + parseAsLibrary: true, + }).success, + ).toBe(true); + + expect(schema.safeParse({ packagePath: 'valid/path', executableName: 123 }).success).toBe( + false, + ); + expect( + schema.safeParse({ packagePath: 'valid/path', arguments: ['arg1', 123] }).success, + ).toBe(false); + expect( + schema.safeParse({ packagePath: 'valid/path', configuration: 'release' }).success, + ).toBe(false); + expect(schema.safeParse({ packagePath: 'valid/path', timeout: '30' }).success).toBe(false); + expect(schema.safeParse({ packagePath: 'valid/path', background: 'true' }).success).toBe( + false, + ); + expect(schema.safeParse({ packagePath: 'valid/path', parseAsLibrary: 'true' }).success).toBe( + false, + ); + + const schemaKeys = Object.keys(swiftPackageRun.schema).sort(); + expect(schemaKeys).toEqual( + [ + 'arguments', + 'background', + 'executableName', + 'packagePath', + 'parseAsLibrary', + 'timeout', + ].sort(), + ); }); }); diff --git a/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts b/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts index 3343dfd2..c09ac011 100644 --- a/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts +++ b/src/mcp/tools/swift-package/__tests__/swift_package_test.test.ts @@ -5,6 +5,7 @@ */ import { describe, it, expect } from 'vitest'; +import * as z from 'zod'; import { createMockExecutor, createMockFileSystemExecutor, @@ -29,31 +30,47 @@ describe('swift_package_test plugin', () => { }); it('should validate schema correctly', () => { - // Test required fields - expect(swiftPackageTest.schema.packagePath.safeParse('/test/package').success).toBe(true); - expect(swiftPackageTest.schema.packagePath.safeParse('').success).toBe(true); + const schema = z.strictObject(swiftPackageTest.schema); - // Test optional fields - expect(swiftPackageTest.schema.testProduct.safeParse('MyTests').success).toBe(true); - expect(swiftPackageTest.schema.testProduct.safeParse(undefined).success).toBe(true); - expect(swiftPackageTest.schema.filter.safeParse('Test.*').success).toBe(true); - expect(swiftPackageTest.schema.filter.safeParse(undefined).success).toBe(true); - expect(swiftPackageTest.schema.configuration.safeParse('debug').success).toBe(true); - expect(swiftPackageTest.schema.configuration.safeParse('release').success).toBe(true); - expect(swiftPackageTest.schema.configuration.safeParse(undefined).success).toBe(true); - expect(swiftPackageTest.schema.parallel.safeParse(true).success).toBe(true); - expect(swiftPackageTest.schema.parallel.safeParse(undefined).success).toBe(true); - expect(swiftPackageTest.schema.showCodecov.safeParse(true).success).toBe(true); - expect(swiftPackageTest.schema.showCodecov.safeParse(undefined).success).toBe(true); - expect(swiftPackageTest.schema.parseAsLibrary.safeParse(true).success).toBe(true); - expect(swiftPackageTest.schema.parseAsLibrary.safeParse(undefined).success).toBe(true); + expect(schema.safeParse({ packagePath: '/test/package' }).success).toBe(true); + expect(schema.safeParse({ packagePath: '' }).success).toBe(true); - // Test invalid inputs - expect(swiftPackageTest.schema.packagePath.safeParse(null).success).toBe(false); - expect(swiftPackageTest.schema.configuration.safeParse('invalid').success).toBe(false); - expect(swiftPackageTest.schema.parallel.safeParse('yes').success).toBe(false); - expect(swiftPackageTest.schema.showCodecov.safeParse('yes').success).toBe(false); - expect(swiftPackageTest.schema.parseAsLibrary.safeParse('yes').success).toBe(false); + expect( + schema.safeParse({ + packagePath: '/test/package', + testProduct: 'MyTests', + filter: 'Test.*', + parallel: true, + showCodecov: true, + parseAsLibrary: true, + }).success, + ).toBe(true); + + expect(schema.safeParse({ packagePath: null }).success).toBe(false); + expect( + schema.safeParse({ packagePath: '/test/package', configuration: 'release' }).success, + ).toBe(false); + expect(schema.safeParse({ packagePath: '/test/package', parallel: 'yes' }).success).toBe( + false, + ); + expect(schema.safeParse({ packagePath: '/test/package', showCodecov: 'yes' }).success).toBe( + false, + ); + expect( + schema.safeParse({ packagePath: '/test/package', parseAsLibrary: 'yes' }).success, + ).toBe(false); + + const schemaKeys = Object.keys(swiftPackageTest.schema).sort(); + expect(schemaKeys).toEqual( + [ + 'filter', + 'packagePath', + 'parseAsLibrary', + 'parallel', + 'showCodecov', + 'testProduct', + ].sort(), + ); }); }); diff --git a/src/mcp/tools/swift-package/swift_package_build.ts b/src/mcp/tools/swift-package/swift_package_build.ts index 95c0d585..ebc54de9 100644 --- a/src/mcp/tools/swift-package/swift_package_build.ts +++ b/src/mcp/tools/swift-package/swift_package_build.ts @@ -5,17 +5,26 @@ import { log } from '../../../utils/logging/index.ts'; import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { ToolResponse } from '../../../types/common.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { + createSessionAwareTool, + getSessionAwareToolSchemaShape, +} from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject -const swiftPackageBuildSchema = z.object({ +const baseSchemaObject = z.object({ packagePath: z.string(), targetName: z.string().optional(), - configuration: z.enum(['debug', 'release']).optional(), + configuration: z.enum(['debug', 'release', 'Debug', 'Release']).optional(), architectures: z.array(z.string()).optional(), parseAsLibrary: z.boolean().optional(), }); +const publicSchemaObject = baseSchemaObject.omit({ + configuration: true, +} as const); + +const swiftPackageBuildSchema = baseSchemaObject; + // Use z.infer for type safety type SwiftPackageBuildParams = z.infer; @@ -73,14 +82,17 @@ export async function swift_package_buildLogic( export default { name: 'swift_package_build', description: 'swift package target build.', - schema: swiftPackageBuildSchema.shape, // MCP SDK compatibility + schema: getSessionAwareToolSchemaShape({ + sessionAware: publicSchemaObject, + legacy: baseSchemaObject, + }), // MCP SDK compatibility annotations: { title: 'Swift Package Build', destructiveHint: true, }, - handler: createTypedTool( - swiftPackageBuildSchema, - swift_package_buildLogic, - getDefaultCommandExecutor, - ), + handler: createSessionAwareTool({ + internalSchema: swiftPackageBuildSchema, + logicFunction: swift_package_buildLogic, + getExecutor: getDefaultCommandExecutor, + }), }; diff --git a/src/mcp/tools/swift-package/swift_package_run.ts b/src/mcp/tools/swift-package/swift_package_run.ts index d6f62c94..23265850 100644 --- a/src/mcp/tools/swift-package/swift_package_run.ts +++ b/src/mcp/tools/swift-package/swift_package_run.ts @@ -6,19 +6,28 @@ import type { CommandExecutor } from '../../../utils/execution/index.ts'; import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { ToolResponse, createTextContent } from '../../../types/common.ts'; import { addProcess } from './active-processes.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { + createSessionAwareTool, + getSessionAwareToolSchemaShape, +} from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject -const swiftPackageRunSchema = z.object({ +const baseSchemaObject = z.object({ packagePath: z.string(), executableName: z.string().optional(), arguments: z.array(z.string()).optional(), - configuration: z.enum(['debug', 'release']).optional(), + configuration: z.enum(['debug', 'release', 'Debug', 'Release']).optional(), timeout: z.number().optional(), background: z.boolean().optional(), parseAsLibrary: z.boolean().optional(), }); +const publicSchemaObject = baseSchemaObject.omit({ + configuration: true, +} as const); + +const swiftPackageRunSchema = baseSchemaObject; + // Use z.infer for type safety type SwiftPackageRunParams = z.infer; @@ -210,14 +219,17 @@ export async function swift_package_runLogic( export default { name: 'swift_package_run', description: 'swift package target run.', - schema: swiftPackageRunSchema.shape, // MCP SDK compatibility + schema: getSessionAwareToolSchemaShape({ + sessionAware: publicSchemaObject, + legacy: baseSchemaObject, + }), // MCP SDK compatibility annotations: { title: 'Swift Package Run', destructiveHint: true, }, - handler: createTypedTool( - swiftPackageRunSchema, - swift_package_runLogic, - getDefaultCommandExecutor, - ), + handler: createSessionAwareTool({ + internalSchema: swiftPackageRunSchema, + logicFunction: swift_package_runLogic, + getExecutor: getDefaultCommandExecutor, + }), }; diff --git a/src/mcp/tools/swift-package/swift_package_test.ts b/src/mcp/tools/swift-package/swift_package_test.ts index 44605ac0..1ce0b02d 100644 --- a/src/mcp/tools/swift-package/swift_package_test.ts +++ b/src/mcp/tools/swift-package/swift_package_test.ts @@ -5,19 +5,28 @@ import { getDefaultCommandExecutor } from '../../../utils/execution/index.ts'; import { createTextResponse, createErrorResponse } from '../../../utils/responses/index.ts'; import { log } from '../../../utils/logging/index.ts'; import { ToolResponse } from '../../../types/common.ts'; -import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { + createSessionAwareTool, + getSessionAwareToolSchemaShape, +} from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject -const swiftPackageTestSchema = z.object({ +const baseSchemaObject = z.object({ packagePath: z.string(), testProduct: z.string().optional(), filter: z.string().optional().describe('regex: pattern'), - configuration: z.enum(['debug', 'release']).optional(), + configuration: z.enum(['debug', 'release', 'Debug', 'Release']).optional(), parallel: z.boolean().optional(), showCodecov: z.boolean().optional(), parseAsLibrary: z.boolean().optional(), }); +const publicSchemaObject = baseSchemaObject.omit({ + configuration: true, +} as const); + +const swiftPackageTestSchema = baseSchemaObject; + // Use z.infer for type safety type SwiftPackageTestParams = z.infer; @@ -83,14 +92,17 @@ export async function swift_package_testLogic( export default { name: 'swift_package_test', description: 'Run swift package target tests.', - schema: swiftPackageTestSchema.shape, // MCP SDK compatibility + schema: getSessionAwareToolSchemaShape({ + sessionAware: publicSchemaObject, + legacy: baseSchemaObject, + }), // MCP SDK compatibility annotations: { title: 'Swift Package Test', destructiveHint: true, }, - handler: createTypedTool( - swiftPackageTestSchema, - swift_package_testLogic, - getDefaultCommandExecutor, - ), + handler: createSessionAwareTool({ + internalSchema: swiftPackageTestSchema, + logicFunction: swift_package_testLogic, + getExecutor: getDefaultCommandExecutor, + }), }; diff --git a/src/mcp/tools/utilities/__tests__/clean.test.ts b/src/mcp/tools/utilities/__tests__/clean.test.ts index c742a739..ac165b4c 100644 --- a/src/mcp/tools/utilities/__tests__/clean.test.ts +++ b/src/mcp/tools/utilities/__tests__/clean.test.ts @@ -21,18 +21,16 @@ describe('clean (unified) tool', () => { expect(schema.safeParse({}).success).toBe(true); expect( schema.safeParse({ - derivedDataPath: '/tmp/Derived', extraArgs: ['--quiet'], - preferXcodebuild: true, platform: 'iOS Simulator', }).success, ).toBe(true); + expect(schema.safeParse({ derivedDataPath: '/tmp/Derived' }).success).toBe(false); + expect(schema.safeParse({ preferXcodebuild: true }).success).toBe(false); expect(schema.safeParse({ configuration: 'Debug' }).success).toBe(false); const schemaKeys = Object.keys(tool.schema).sort(); - expect(schemaKeys).toEqual( - ['derivedDataPath', 'extraArgs', 'platform', 'preferXcodebuild'].sort(), - ); + expect(schemaKeys).toEqual(['extraArgs', 'platform'].sort()); }); it('handler validation: error when neither projectPath nor workspacePath provided', async () => { diff --git a/src/mcp/tools/utilities/clean.ts b/src/mcp/tools/utilities/clean.ts index 40361efa..0df58ecb 100644 --- a/src/mcp/tools/utilities/clean.ts +++ b/src/mcp/tools/utilities/clean.ts @@ -144,6 +144,8 @@ const publicSchemaObject = baseSchemaObject.omit({ workspacePath: true, scheme: true, configuration: true, + derivedDataPath: true, + preferXcodebuild: true, } as const); export default { diff --git a/src/utils/session-store.ts b/src/utils/session-store.ts index e61c691b..520e97e6 100644 --- a/src/utils/session-store.ts +++ b/src/utils/session-store.ts @@ -11,6 +11,10 @@ export type SessionDefaults = { useLatestOS?: boolean; arch?: 'arm64' | 'x86_64'; suppressWarnings?: boolean; + derivedDataPath?: string; + preferXcodebuild?: boolean; + platform?: string; + bundleId?: string; }; class SessionStore { From 4efcf91e50119472bb86a94bf5e57bacd2c836aa Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Sun, 25 Jan 2026 23:28:57 +0000 Subject: [PATCH 4/6] Add manage-workflows tool to allow agents to add new workflows at run-time Also renames describe-ui tool to snapshot-ui tool. --- .cursorrules | 1 - .vscode/extensions.json | 4 +- .vscode/settings.json | 6 +- CHANGELOG.md | 2 + docs/CONFIGURATION.md | 9 +- docs/TOOLS.md | 154 +++++++++--------- docs/dev/ARCHITECTURE.md | 2 +- docs/dev/MANUAL_TESTING.md | 2 +- docs/dev/PLUGIN_DEVELOPMENT.md | 4 +- docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md | 4 +- docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md | 4 +- docs/dev/TESTING.md | 2 +- docs/dev/session-aware-migration-todo.md | 2 +- docs/investigations/issue-163.md | 4 +- ...describe-ui-empty-after-debugger-resume.md | 24 +-- src/core/generated-plugins.ts | 111 +++++++------ src/core/plugin-registry.ts | 4 + src/core/plugin-types.ts | 2 + .../__tests__/get_device_app_path.test.ts | 4 +- .../__tests__/install_app_device.test.ts | 2 +- .../__tests__/launch_app_device.test.ts | 2 +- .../device/__tests__/list_devices.test.ts | 4 +- .../device/__tests__/stop_app_device.test.ts | 2 +- src/mcp/tools/doctor/__tests__/doctor.test.ts | 8 +- src/mcp/tools/doctor/doctor.ts | 16 +- src/mcp/tools/doctor/lib/doctor.deps.ts | 51 +----- .../__tests__/stop_device_log_cap.test.ts | 4 +- .../__tests__/stop_sim_log_cap.test.ts | 4 +- .../tools/macos/__tests__/build_macos.test.ts | 2 +- .../macos/__tests__/build_run_macos.test.ts | 2 +- .../macos/__tests__/launch_mac_app.test.ts | 4 +- .../macos/__tests__/stop_mac_app.test.ts | 4 +- .../__tests__/list_schemes.test.ts | 2 +- .../__tests__/scaffold_macos_project.test.ts | 4 +- .../__tests__/session_clear_defaults.test.ts | 2 +- .../__tests__/session_set_defaults.test.ts | 2 +- .../__tests__/session_show_defaults.test.ts | 2 +- .../simulator/__tests__/boot_sim.test.ts | 2 +- .../simulator/__tests__/screenshot.test.ts | 2 +- src/mcp/tools/simulator/describe_ui.ts | 2 - src/mcp/tools/simulator/screenshot.ts | 2 +- src/mcp/tools/simulator/snapshot_ui.ts | 2 + .../__tests__/button.test.ts | 0 .../__tests__/gesture.test.ts | 0 .../__tests__/index.test.ts | 4 +- .../__tests__/key_press.test.ts | 0 .../__tests__/key_sequence.test.ts | 0 .../__tests__/long_press.test.ts | 2 +- .../__tests__/screenshot.test.ts | 0 .../__tests__/snapshot_ui.test.ts} | 34 ++-- .../__tests__/swipe.test.ts | 4 +- .../__tests__/tap.test.ts | 12 +- .../__tests__/touch.test.ts | 10 +- .../__tests__/type_text.test.ts | 0 .../{ui-testing => ui-automation}/button.ts | 0 .../{ui-testing => ui-automation}/gesture.ts | 0 .../{ui-testing => ui-automation}/index.ts | 2 +- .../key_press.ts | 0 .../key_sequence.ts | 0 .../long_press.ts | 16 +- .../screenshot.ts | 0 .../snapshot_ui.ts} | 42 ++--- .../{ui-testing => ui-automation}/swipe.ts | 14 +- .../{ui-testing => ui-automation}/tap.ts | 14 +- .../{ui-testing => ui-automation}/touch.ts | 16 +- .../type_text.ts | 0 .../__tests__/manage_workflows.test.ts | 68 ++++++++ src/mcp/tools/workflow-discovery/index.ts | 4 + .../workflow-discovery/manage_workflows.ts | 49 ++++++ src/server/bootstrap.ts | 6 +- src/server/server-state.ts | 13 ++ src/server/server.ts | 9 +- .../__tests__/workflow-selection.test.ts | 63 ++++++- src/utils/plugin-registry/index.ts | 6 +- src/utils/runtime-registry.ts | 35 ---- src/utils/tool-registry.ts | 117 ++++++++----- src/utils/workflow-selection.ts | 85 ++++++++-- tools.compact.json | 2 +- tools.new.json | 2 +- tools_original.json | 10 +- 80 files changed, 669 insertions(+), 442 deletions(-) delete mode 120000 .cursorrules delete mode 100644 src/mcp/tools/simulator/describe_ui.ts create mode 100644 src/mcp/tools/simulator/snapshot_ui.ts rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/button.test.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/gesture.test.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/index.test.ts (90%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/key_press.test.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/key_sequence.test.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/long_press.test.ts (99%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/screenshot.test.ts (100%) rename src/mcp/tools/{ui-testing/__tests__/describe_ui.test.ts => ui-automation/__tests__/snapshot_ui.test.ts} (91%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/swipe.test.ts (98%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/tap.test.ts (97%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/touch.test.ts (97%) rename src/mcp/tools/{ui-testing => ui-automation}/__tests__/type_text.test.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/button.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/gesture.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/index.ts (85%) rename src/mcp/tools/{ui-testing => ui-automation}/key_press.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/key_sequence.ts (100%) rename src/mcp/tools/{ui-testing => ui-automation}/long_press.ts (92%) rename src/mcp/tools/{ui-testing => ui-automation}/screenshot.ts (100%) rename src/mcp/tools/{ui-testing/describe_ui.ts => ui-automation/snapshot_ui.ts} (85%) rename src/mcp/tools/{ui-testing => ui-automation}/swipe.ts (93%) rename src/mcp/tools/{ui-testing => ui-automation}/tap.ts (94%) rename src/mcp/tools/{ui-testing => ui-automation}/touch.ts (92%) rename src/mcp/tools/{ui-testing => ui-automation}/type_text.ts (100%) create mode 100644 src/mcp/tools/workflow-discovery/__tests__/manage_workflows.test.ts create mode 100644 src/mcp/tools/workflow-discovery/index.ts create mode 100644 src/mcp/tools/workflow-discovery/manage_workflows.ts create mode 100644 src/server/server-state.ts delete mode 100644 src/utils/runtime-registry.ts diff --git a/.cursorrules b/.cursorrules deleted file mode 120000 index 47dc3e3d..00000000 --- a/.cursorrules +++ /dev/null @@ -1 +0,0 @@ -AGENTS.md \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 59daa7a0..a74981ad 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,8 +1,6 @@ { "recommendations": [ - "dbaeumer.vscode-eslint" - ], - "unwantedRecommendations": [ + "dbaeumer.vscode-eslint", "esbenp.prettier-vscode" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index dada907e..915880e1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,12 +20,12 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "editor.defaultFormatter": "vscode.typescript-language-features", + "editor.defaultFormatter": "esbenp.prettier-vscode", "[typescript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" + "editor.defaultFormatter": "esbenp.prettier-vscode" }, "terminal.integrated.shellIntegration.decorationsEnabled": "never", "[json]": { diff --git a/CHANGELOG.md b/CHANGELOG.md index f6d309de..40867193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ - Add DAP-based debugger backend and simulator debugging toolset (attach, breakpoints, stack, variables, LLDB command). - Add session-status MCP resource with session identifiers. - Add UI automation guard that blocks UI tools when the debugger is paused. +- Add manage-workflows tool for live workflow selection updates. ### Changed - Migrate to Zod v4. - Improve session default handling (reconcile mutual exclusivity and ignore explicit undefined clears). +- Auto-include workflow-discovery when workflow selection is configured. ### Fixed - Update UI automation guard guidance to point at `debug_continue` when paused. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a8216adc..5a0f874e 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -17,7 +17,7 @@ XcodeBuildMCP is configured through environment variables provided by your MCP c ## Workflow selection -By default, XcodeBuildMCP loads all tools at startup. If you want a smaller tool surface for a specific workflow, 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. +By default, XcodeBuildMCP loads all tools at startup. If you want a smaller tool surface for a specific workflow, 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. When `XCODEBUILDMCP_DEBUG=true`, the `doctor` workflow is also auto-included. **Available workflows:** - `device` (14 tools) - iOS Device Development @@ -28,6 +28,7 @@ By default, XcodeBuildMCP loads all tools at startup. If you want a smaller tool - `project-scaffolding` (2 tools) - Project Scaffolding - `utilities` (1 tool) - Project Utilities - `session-management` (3 tools) - session-management +- `workflow-discovery` (1 tool) - Workflow Discovery - `debugging` (8 tools) - Simulator Debugging - `simulator-management` (8 tools) - Simulator Management - `swift-package` (6 tools) - Swift Package Manager @@ -41,6 +42,12 @@ XcodeBuildMCP includes experimental support for incremental builds. This feature > [!IMPORTANT] > Incremental builds are highly experimental and your mileage may vary. Please report issues to the [issue tracker](https://github.com/cameroncooke/XcodeBuildMCP/issues). +## Experimental workflow discovery + +Set `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY=true` to auto-include the `workflow-discovery` workflow at startup. + +The workflow discovery tool lets agents and clients enable or disable workflows at runtime. This can reduce upfront context by loading only what is needed as the session evolves. Note: most clients do not yet support the MCP notifications required for an agent harness to re-query the tool list after changes. + ## Session-aware opt-out By default, XcodeBuildMCP uses a session-aware mode: the LLM (or client) sets shared defaults once (simulator, device, project/workspace, scheme, etc.), and all tools reuse them. This cuts context bloat not just in each call payload, but also in the tool schemas themselves. diff --git a/docs/TOOLS.md b/docs/TOOLS.md index f3f91f0a..b5e10f43 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -1,125 +1,129 @@ # XcodeBuildMCP Tools Reference -XcodeBuildMCP provides 71 tools organized into 13 workflow groups for comprehensive Apple development workflows. +XcodeBuildMCP provides 72 tools organized into 14 workflow groups for comprehensive Apple development workflows. ## Workflow Groups ### iOS Device Development (`device`) **Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting physical devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro). Build, test, deploy, and debug apps on real hardware. (7 tools) -- `build_device` - Builds an app for a connected device. -- `get_device_app_path` - Retrieves the built app path for a connected device. -- `install_app_device` - Installs an app on a connected device. -- `launch_app_device` - Launches an app on a connected device. -- `list_devices` - Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing. -- `stop_app_device` - Stops a running app on a connected device. -- `test_device` - Runs tests on a physical Apple device. +- `build_device` - Build for device. +- `get_device_app_path` - Get device built app path. +- `install_app_device` - Install app on device. +- `launch_app_device` - Launch app on device. +- `list_devices` - List connected devices. +- `stop_app_device` - Stop device app. +- `test_device` - Test on device. ### iOS Simulator Development (`simulator`) **Purpose**: Complete iOS development workflow for both .xcodeproj and .xcworkspace files targeting simulators. Build, test, deploy, and interact with iOS apps on simulators. (12 tools) -- `boot_sim` - Boots an iOS simulator. -- `build_run_sim` - Builds and runs an app on an iOS simulator. -- `build_sim` - Builds an app for an iOS simulator. -- `get_sim_app_path` - Retrieves the built app path for an iOS simulator. -- `install_app_sim` - Installs an app in an iOS simulator. -- `launch_app_logs_sim` - Launches an app in an iOS simulator and captures its logs. -- `launch_app_sim` - Launches an app in an iOS simulator. -- `list_sims` - Lists available iOS simulators with their UUIDs. -- `open_sim` - Opens the iOS Simulator app. -- `record_sim_video` - Starts or stops video capture for an iOS simulator. -- `stop_app_sim` - Stops an app running in an iOS simulator. -- `test_sim` - Runs tests on an iOS simulator. +- `boot_sim` - Boot iOS simulator. +- `build_run_sim` - Build and run iOS sim. +- `build_sim` - Build for iOS sim. +- `get_sim_app_path` - Get sim built app path. +- `install_app_sim` - Install app on sim. +- `launch_app_logs_sim` - Launch sim app with logs. +- `launch_app_sim` - Launch app on simulator. +- `list_sims` - List iOS simulators. +- `open_sim` - Open Simulator app. +- `record_sim_video` - Record sim video. +- `stop_app_sim` - Stop sim app. +- `test_sim` - Test on iOS sim. ### Log Capture & Management (`logging`) **Purpose**: Log capture and management tools for iOS simulators and physical devices. Start, stop, and analyze application and system logs during development and testing. (4 tools) -- `start_device_log_cap` - Starts log capture on a connected device. -- `start_sim_log_cap` - Starts capturing logs from a specified simulator. Returns a session ID. By default, captures only structured logs. -- `stop_device_log_cap` - Stops an active Apple device log capture session and returns the captured logs. -- `stop_sim_log_cap` - Stops an active simulator log capture session and returns the captured logs. +- `start_device_log_cap` - Start device log capture. +- `start_sim_log_cap` - Start sim log capture. +- `stop_device_log_cap` - Stop device log capture. +- `stop_sim_log_cap` - Stop sim log capture. ### macOS Development (`macos`) **Purpose**: Complete macOS development workflow for both .xcodeproj and .xcworkspace files. Build, test, deploy, and manage macOS applications. (6 tools) -- `build_macos` - Builds a macOS app. -- `build_run_macos` - Builds and runs a macOS app. -- `get_mac_app_path` - Retrieves the built macOS app bundle path. -- `launch_mac_app` - Launches a macOS application. Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app. -- `stop_mac_app` - Stops a running macOS application. Can stop by app name or process ID. -- `test_macos` - Runs tests for a macOS target. +- `build_macos` - Build macOS app. +- `build_run_macos` - Build and run macOS app. +- `get_mac_app_path` - Get macOS built app path. +- `launch_mac_app` - Launch macOS app. +- `stop_mac_app` - Stop macOS app. +- `test_macos` - Test macOS target. ### Project Discovery (`project-discovery`) **Purpose**: Discover and examine Xcode projects, workspaces, and Swift packages. Analyze project structure, schemes, build settings, and bundle information. (5 tools) - `discover_projs` - Scans a directory (defaults to workspace root) to find Xcode project (.xcodeproj) and workspace (.xcworkspace) files. -- `get_app_bundle_id` - Extracts the bundle identifier from an app bundle (.app) for any Apple platform (iOS, iPadOS, watchOS, tvOS, visionOS). -- `get_mac_bundle_id` - Extracts the bundle identifier from a macOS app bundle (.app). Note: In some environments, this tool may be prefixed as mcp0_get_macos_bundle_id. -- `list_schemes` - Lists schemes for a project or workspace. -- `show_build_settings` - Shows xcodebuild build settings. +- `get_app_bundle_id` - Extract bundle id from .app. +- `get_mac_bundle_id` - Extract bundle id from macOS .app. +- `list_schemes` - List Xcode schemes. +- `show_build_settings` - Show build settings. ### Project Scaffolding (`project-scaffolding`) **Purpose**: Tools for creating new iOS and macOS projects from templates. Bootstrap new applications with best practices, standard configurations, and modern project structures. (2 tools) -- `scaffold_ios_project` - Scaffold a new iOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper iOS configuration. -- `scaffold_macos_project` - Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration. +- `scaffold_ios_project` - Scaffold iOS project. +- `scaffold_macos_project` - Scaffold macOS project. ### Project Utilities (`utilities`) **Purpose**: Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files. (1 tools) -- `clean` - Cleans build products with xcodebuild. +- `clean` - Clean build products. ### session-management (`session-management`) **Purpose**: Manage session defaults for projectPath/workspacePath, scheme, configuration, simulatorName/simulatorId, deviceId, useLatestOS and arch. These defaults are required by many tools and must be set before attempting to call tools that would depend on these values. (3 tools) -- `session_clear_defaults` - Clear selected or all session defaults. -- `session_set_defaults` - Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set all relevant defaults up front in a single call (e.g., project/workspace, scheme, simulator or device ID, useLatestOS) to avoid iterative prompts; only set the keys your workflow needs. -- `session_show_defaults` - Show current session defaults. +- `session_clear_defaults` - Clear session defaults. +- `session_set_defaults` - Set the session defaults, should be called at least once to set tool defaults. +- `session_show_defaults` - Show session defaults. ### Simulator Debugging (`debugging`) **Purpose**: Interactive iOS Simulator debugging tools: attach LLDB, manage breakpoints, inspect stack/variables, and run LLDB commands. (8 tools) -- `debug_attach_sim` - Attach LLDB to a running iOS simulator app. Provide bundleId or pid, plus simulator defaults. -- `debug_breakpoint_add` - Add a breakpoint by file/line or function name for the active debug session. -- `debug_breakpoint_remove` - Remove a breakpoint by id for the active debug session. -- `debug_continue` - Resume execution in the active debug session or a specific debugSessionId. -- `debug_detach` - Detach the current debugger session or a specific debugSessionId. -- `debug_lldb_command` - Run an arbitrary LLDB command within the active debug session. -- `debug_stack` - Return a thread backtrace from the active debug session. -- `debug_variables` - Return variables for a selected frame in the active debug session. +- `debug_attach_sim` - Attach LLDB to sim app. +- `debug_breakpoint_add` - Add breakpoint. +- `debug_breakpoint_remove` - Remove breakpoint. +- `debug_continue` - Continue debug session. +- `debug_detach` - Detach debugger. +- `debug_lldb_command` - Run LLDB command. +- `debug_stack` - Get backtrace. +- `debug_variables` - Get frame variables. ### Simulator Management (`simulator-management`) **Purpose**: Tools for managing simulators from booting, opening simulators, listing simulators, stopping simulators, erasing simulator content and settings, and setting simulator environment options like location, network, statusbar and appearance. (5 tools) -- `erase_sims` - Erases a simulator by UDID. -- `reset_sim_location` - Resets the simulator's location to default. -- `set_sim_appearance` - Sets the appearance mode (dark/light) of an iOS simulator. -- `set_sim_location` - Sets a custom GPS location for the simulator. -- `sim_statusbar` - Sets the data network indicator in the iOS simulator status bar. Use "clear" to reset all overrides, or specify a network type (hide, wifi, 3g, 4g, lte, lte-a, lte+, 5g, 5g+, 5g-uwb, 5g-uc). +- `erase_sims` - Erase simulator. +- `reset_sim_location` - Reset sim location. +- `set_sim_appearance` - Set sim appearance. +- `set_sim_location` - Set sim location. +- `sim_statusbar` - Set sim status bar network. ### Swift Package Manager (`swift-package`) **Purpose**: Swift Package Manager operations for building, testing, running, and managing Swift packages and dependencies. Complete SPM workflow support. (6 tools) -- `swift_package_build` - Builds a Swift Package with swift build -- `swift_package_clean` - Cleans Swift Package build artifacts and derived data -- `swift_package_list` - Lists currently running Swift Package processes -- `swift_package_run` - Runs an executable target from a Swift Package with swift run -- `swift_package_stop` - Stops a running Swift Package executable started with swift_package_run -- `swift_package_test` - Runs tests for a Swift Package with swift test +- `swift_package_build` - swift package target build. +- `swift_package_clean` - swift package clean. +- `swift_package_list` - List SwiftPM processes. +- `swift_package_run` - swift package target run. +- `swift_package_stop` - Stop SwiftPM run. +- `swift_package_test` - Run swift package target tests. ### System Doctor (`doctor`) **Purpose**: Debug tools and system doctor for troubleshooting XcodeBuildMCP server, development environment, and tool availability. (1 tools) -- `doctor` - Provides comprehensive information about the MCP server environment, available dependencies, and configuration status. -### UI Testing & Automation (`ui-testing`) +- `doctor` - MCP environment info. +### UI Automation (`ui-automation`) **Purpose**: UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows. (11 tools) -- `button` - Press hardware button on iOS simulator. Supported buttons: apple-pay, home, lock, side-button, siri -- `describe_ui` - Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation. Requires the target process to be running; paused debugger/breakpoints can yield an empty tree. -- `gesture` - Perform gesture on iOS simulator using preset gestures: scroll-up, scroll-down, scroll-left, scroll-right, swipe-from-left-edge, swipe-from-right-edge, swipe-from-top-edge, swipe-from-bottom-edge -- `key_press` - Press a single key by keycode on the simulator. Common keycodes: 40=Return, 42=Backspace, 43=Tab, 44=Space, 58-67=F1-F10. -- `key_sequence` - Press key sequence using HID keycodes on iOS simulator with configurable delay -- `long_press` - Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots). -- `screenshot` - Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots). -- `swipe` - Swipe from one point to another. Use describe_ui for precise coordinates (don't guess from screenshots). Supports configurable timing. -- `tap` - Tap at specific coordinates or target elements by accessibility id or label. Use describe_ui to get precise element coordinates prior to using x/y parameters (don't guess from screenshots). Supports optional timing delays. -- `touch` - Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots). -- `type_text` - Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type. +- `button` - Press simulator hardware button. +- `gesture` - Simulator gesture preset. +- `key_press` - Press key by keycode. +- `key_sequence` - Press a sequence of keys by their keycodes. +- `long_press` - Long press at coords. +- `screenshot` - Capture screenshot. +- `snapshot_ui` - Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements. +- `swipe` - Swipe between points. +- `tap` - Tap coordinate or element. +- `touch` - Touch down/up at coords. +- `type_text` - Type text. +### workflow-discovery (`workflow-discovery`) +**Purpose**: workflow-discovery related tools (1 tools) + +- `manage_workflows` - Workflows are groups of tools exposed by XcodeBuildMCP. By default, not all workflows (and therefore tools) are enabled; only simulator tools are enabled by default. Some workflows are mandatory and can't be disabled. Available workflows: ${availableWorkflows} ## Summary Statistics -- **Total Tools**: 71 canonical tools + 22 re-exports = 93 total -- **Workflow Groups**: 13 +- **Total Tools**: 72 canonical tools + 22 re-exports = 94 total +- **Workflow Groups**: 14 --- -*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2026-01-08* +*This documentation is automatically generated by `scripts/update-tools-docs.ts` using static analysis. Last updated: 2026-01-25* diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index f466b7d2..c0a1de14 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -47,7 +47,7 @@ XcodeBuildMCP is a Model Context Protocol (MCP) server that exposes Xcode operat 4. **Plugin & Resource Loading (Runtime)** - At runtime, `loadPlugins()` and `loadResources()` use the generated loaders from the previous step - All workflow loaders are executed at startup to register tools - - If `XCODEBUILDMCP_ENABLED_WORKFLOWS` is set, only those workflows (plus `session-management`) are registered +- If `XCODEBUILDMCP_ENABLED_WORKFLOWS` is set, only those workflows (plus `session-management`) are registered; `workflow-discovery` is only auto-included when `XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY=true` 5. **Tool Registration** - Discovered tools automatically registered with server using pre-generated maps diff --git a/docs/dev/MANUAL_TESTING.md b/docs/dev/MANUAL_TESTING.md index cee3b565..77fa39ba 100644 --- a/docs/dev/MANUAL_TESTING.md +++ b/docs/dev/MANUAL_TESTING.md @@ -260,7 +260,7 @@ fi - `test_*` tools - Run test suites 6. **UI Automation Tools** (depend on running apps): - - `describe_ui`, `screenshot`, `tap`, etc. + - `snapshot_ui`, `screenshot`, `tap`, etc. **MANDATORY: Record Key Outputs** diff --git a/docs/dev/PLUGIN_DEVELOPMENT.md b/docs/dev/PLUGIN_DEVELOPMENT.md index 15f1f5e7..13d42efe 100644 --- a/docs/dev/PLUGIN_DEVELOPMENT.md +++ b/docs/dev/PLUGIN_DEVELOPMENT.md @@ -449,7 +449,7 @@ for (const plugin of plugins.values()) { ### 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. +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 @@ -713,7 +713,7 @@ Instead of using generic descriptions like "Additional Tools: Simulator manageme **โœ… Correct:** ```markdown - `boot_sim`, `install_app_sim`, `launch_app_sim`, `list_sims`, `open_sim` -- `describe_ui`, `screenshot`, `start_sim_log_cap`, `stop_sim_log_cap` +- `snapshot_ui`, `screenshot`, `start_sim_log_cap`, `stop_sim_log_cap` ``` #### 4. Systematic Documentation Update Steps diff --git a/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md b/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md index a3ad909a..6d6a9af5 100644 --- a/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md +++ b/docs/dev/RELOADEROO_FOR_XCODEBUILDMCP.md @@ -245,9 +245,9 @@ npx reloaderoo@latest --help ```bash npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -- node build/index.js ``` -- **`describe_ui`**: Gets the UI hierarchy of the current screen. +- **`snapshot_ui`**: Gets the UI hierarchy of the current screen. ```bash - npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js + npx reloaderoo@latest inspect call-tool snapshot_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -- node build/index.js ``` - **`gesture`**: Performs a pre-defined gesture. ```bash diff --git a/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md b/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md index eefada1d..5de23697 100644 --- a/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md +++ b/docs/dev/RELOADEROO_XCODEBUILDMCP_PRIMER.md @@ -268,9 +268,9 @@ Use jq to parse the output to get just the content response: ```bash npx reloaderoo@latest inspect call-tool button --params '{"simulatorUuid": "SIMULATOR_UUID", "buttonType": "home"}' -q -- npx xcodebuildmcp@latest ``` -- **`describe_ui`**: Gets the UI hierarchy of the current screen. +- **`snapshot_ui`**: Gets the UI hierarchy of the current screen. ```bash - npx reloaderoo@latest inspect call-tool describe_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest + npx reloaderoo@latest inspect call-tool snapshot_ui --params '{"simulatorUuid": "SIMULATOR_UUID"}' -q -- npx xcodebuildmcp@latest ``` - **`gesture`**: Performs a pre-defined gesture. ```bash diff --git a/docs/dev/TESTING.md b/docs/dev/TESTING.md index ec8756be..100a6f78 100644 --- a/docs/dev/TESTING.md +++ b/docs/dev/TESTING.md @@ -753,7 +753,7 @@ fi - `test_*` tools - Run test suites 6. **UI Automation Tools** (depend on running apps): - - `describe_ui`, `screenshot`, `tap`, etc. + - `snapshot_ui`, `screenshot`, `tap`, etc. ### MANDATORY: Record Key Outputs diff --git a/docs/dev/session-aware-migration-todo.md b/docs/dev/session-aware-migration-todo.md index 2da02f59..4230f338 100644 --- a/docs/dev/session-aware-migration-todo.md +++ b/docs/dev/session-aware-migration-todo.md @@ -52,7 +52,7 @@ Reference: [session_management_plan.md](session_management_plan.md) ## AXe UI Testing Tools - [x] `src/mcp/tools/ui-testing/button.ts` โ€” session defaults: `simulatorId` (hydrate `simulatorUuid`). -- [x] `src/mcp/tools/ui-testing/describe_ui.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`). diff --git a/docs/investigations/issue-163.md b/docs/investigations/issue-163.md index 45fa41ab..64071a7f 100644 --- a/docs/investigations/issue-163.md +++ b/docs/investigations/issue-163.md @@ -4,7 +4,7 @@ Smithery installs ship only the compiled entrypoint, while the server hard-requires a bundled `bundled/axe` path derived from `process.argv[1]`. This makes UI automation (and simulator video capture) fail even when system `axe` exists on PATH, and Doctor can report contradictory statuses. ## Symptoms -- UI automation tools (`describe_ui`, `tap`, `swipe`, etc.) fail with "Bundled axe tool not found. UI automation features are not available." +- UI automation tools (`snapshot_ui`, `tap`, `swipe`, etc.) fail with "Bundled axe tool not found. UI automation features are not available." - `doctor` reports system axe present, but UI automation unavailable due to missing bundled binary. - Smithery cache lacks `bundled/axe` directory; only `index.cjs`, `manifest.json`, `.metadata.json` present. @@ -25,7 +25,7 @@ Smithery installs ship only the compiled entrypoint, while the server hard-requi ### 2026-01-06 - UI automation and video capture gating **Hypothesis:** UI tools and video capture preflight fail when `getAxePath()` returns null. **Findings:** UI tools call `getAxePath()` and throw `DependencyError` if absent; `record_sim_video` preflights `areAxeToolsAvailable()` and `isAxeAtLeastVersion()`; `startSimulatorVideoCapture` returns error if `getAxePath()` is null. -**Evidence:** `src/mcp/tools/ui-testing/describe_ui.ts:150-164`, `src/mcp/tools/simulator/record_sim_video.ts:80-88`, `src/utils/video_capture.ts:92-99` +**Evidence:** `src/mcp/tools/ui-testing/snapshot_ui.ts:150-164`, `src/mcp/tools/simulator/record_sim_video.ts:80-88`, `src/utils/video_capture.ts:92-99` **Conclusion:** Confirmed. Missing bundled binary blocks all UI automation and simulator video capture. ### 2026-01-06 - Doctor output inconsistency diff --git a/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md b/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md index c67ad5ee..85d85eb1 100644 --- a/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md +++ b/docs/investigations/issue-describe-ui-empty-after-debugger-resume.md @@ -1,12 +1,12 @@ -# RCA: describe_ui returns empty tree after debugger resume +# RCA: snapshot_ui returns empty tree after debugger resume ## Summary -When the app is stopped under LLDB (breakpoints hit), the `describe_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. +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. -- `describe_ui` can return misleading empty trees even though the app is running in the simulator. +- `snapshot_ui` can return misleading empty trees even though the app is running in the simulator. ## Environment - App: Calculator (example project) @@ -19,35 +19,35 @@ When the app is stopped under LLDB (breakpoints hit), the `describe_ui` tool fre 3. `debug_lldb_command` -> `continue`. 4. Tap a button (e.g., "7") so breakpoints fire. 5. `debug_lldb_command` -> `continue`. -6. Call `describe_ui` immediately after resume. +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, `describe_ui` often returns: +- 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, `describe_ui` returns the full tree immediately. -- Detaching the debugger also restores `describe_ui` output. +- 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 `describe_ui` returns an empty hierarchy. +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`, `describe_ui` returns a full hierarchy (buttons + display values). +- After removing breakpoints and `continue`, `snapshot_ui` returns a full hierarchy (buttons + display values). ## Current Workarounds -- Clear or remove breakpoints before calling `describe_ui`. +- Clear or remove breakpoints before calling `snapshot_ui`. - Detach the debugger to allow the app to run normally. ## Recommendations -- Document that `describe_ui` requires the target process to be running (not stopped under LLDB). +- 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 `describe_ui` immediately after breakpoints unless resumed and confirmed running. + - 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/src/core/generated-plugins.ts b/src/core/generated-plugins.ts index b22e2d80..f293cc73 100644 --- a/src/core/generated-plugins.ts +++ b/src/core/generated-plugins.ts @@ -201,29 +201,29 @@ export const WORKFLOW_LOADERS = { const tool_1 = await import('../mcp/tools/simulator/build_run_sim.ts').then((m) => m.default); const tool_2 = await import('../mcp/tools/simulator/build_sim.ts').then((m) => m.default); const tool_3 = await import('../mcp/tools/simulator/clean.ts').then((m) => m.default); - const tool_4 = await import('../mcp/tools/simulator/describe_ui.ts').then((m) => m.default); - const tool_5 = await import('../mcp/tools/simulator/discover_projs.ts').then((m) => m.default); - const tool_6 = await import('../mcp/tools/simulator/get_app_bundle_id.ts').then( + const tool_4 = await import('../mcp/tools/simulator/discover_projs.ts').then((m) => m.default); + const tool_5 = await import('../mcp/tools/simulator/get_app_bundle_id.ts').then( (m) => m.default, ); - const tool_7 = await import('../mcp/tools/simulator/get_sim_app_path.ts').then( + const tool_6 = await import('../mcp/tools/simulator/get_sim_app_path.ts').then( (m) => m.default, ); - const tool_8 = await import('../mcp/tools/simulator/install_app_sim.ts').then((m) => m.default); - const tool_9 = await import('../mcp/tools/simulator/launch_app_logs_sim.ts').then( + const tool_7 = await import('../mcp/tools/simulator/install_app_sim.ts').then((m) => m.default); + const tool_8 = await import('../mcp/tools/simulator/launch_app_logs_sim.ts').then( (m) => m.default, ); - const tool_10 = await import('../mcp/tools/simulator/launch_app_sim.ts').then((m) => m.default); - const tool_11 = await import('../mcp/tools/simulator/list_schemes.ts').then((m) => m.default); - const tool_12 = await import('../mcp/tools/simulator/list_sims.ts').then((m) => m.default); - const tool_13 = await import('../mcp/tools/simulator/open_sim.ts').then((m) => m.default); - const tool_14 = await import('../mcp/tools/simulator/record_sim_video.ts').then( + const tool_9 = await import('../mcp/tools/simulator/launch_app_sim.ts').then((m) => m.default); + const tool_10 = await import('../mcp/tools/simulator/list_schemes.ts').then((m) => m.default); + const tool_11 = await import('../mcp/tools/simulator/list_sims.ts').then((m) => m.default); + const tool_12 = await import('../mcp/tools/simulator/open_sim.ts').then((m) => m.default); + const tool_13 = await import('../mcp/tools/simulator/record_sim_video.ts').then( (m) => m.default, ); - const tool_15 = await import('../mcp/tools/simulator/screenshot.ts').then((m) => m.default); - const tool_16 = await import('../mcp/tools/simulator/show_build_settings.ts').then( + const tool_14 = await import('../mcp/tools/simulator/screenshot.ts').then((m) => m.default); + const tool_15 = await import('../mcp/tools/simulator/show_build_settings.ts').then( (m) => m.default, ); + const tool_16 = await import('../mcp/tools/simulator/snapshot_ui.ts').then((m) => m.default); const tool_17 = await import('../mcp/tools/simulator/stop_app_sim.ts').then((m) => m.default); const tool_18 = await import('../mcp/tools/simulator/test_sim.ts').then((m) => m.default); @@ -233,19 +233,19 @@ export const WORKFLOW_LOADERS = { build_run_sim: tool_1, build_sim: tool_2, clean: tool_3, - describe_ui: tool_4, - discover_projs: tool_5, - get_app_bundle_id: tool_6, - get_sim_app_path: tool_7, - install_app_sim: tool_8, - launch_app_logs_sim: tool_9, - launch_app_sim: tool_10, - list_schemes: tool_11, - list_sims: tool_12, - open_sim: tool_13, - record_sim_video: tool_14, - screenshot: tool_15, - show_build_settings: tool_16, + discover_projs: tool_4, + get_app_bundle_id: tool_5, + get_sim_app_path: tool_6, + install_app_sim: tool_7, + launch_app_logs_sim: tool_8, + launch_app_sim: tool_9, + list_schemes: tool_10, + list_sims: tool_11, + open_sim: tool_12, + record_sim_video: tool_13, + screenshot: tool_14, + show_build_settings: tool_15, + snapshot_ui: tool_16, stop_app_sim: tool_17, test_sim: tool_18, }; @@ -320,29 +320,31 @@ export const WORKFLOW_LOADERS = { swift_package_test: tool_5, }; }, - 'ui-testing': async () => { - const { workflow } = await import('../mcp/tools/ui-testing/index.ts'); - const tool_0 = await import('../mcp/tools/ui-testing/button.ts').then((m) => m.default); - const tool_1 = await import('../mcp/tools/ui-testing/describe_ui.ts').then((m) => m.default); - const tool_2 = await import('../mcp/tools/ui-testing/gesture.ts').then((m) => m.default); - const tool_3 = await import('../mcp/tools/ui-testing/key_press.ts').then((m) => m.default); - const tool_4 = await import('../mcp/tools/ui-testing/key_sequence.ts').then((m) => m.default); - const tool_5 = await import('../mcp/tools/ui-testing/long_press.ts').then((m) => m.default); - const tool_6 = await import('../mcp/tools/ui-testing/screenshot.ts').then((m) => m.default); - const tool_7 = await import('../mcp/tools/ui-testing/swipe.ts').then((m) => m.default); - const tool_8 = await import('../mcp/tools/ui-testing/tap.ts').then((m) => m.default); - const tool_9 = await import('../mcp/tools/ui-testing/touch.ts').then((m) => m.default); - const tool_10 = await import('../mcp/tools/ui-testing/type_text.ts').then((m) => m.default); + 'ui-automation': async () => { + const { workflow } = await import('../mcp/tools/ui-automation/index.ts'); + const tool_0 = await import('../mcp/tools/ui-automation/button.ts').then((m) => m.default); + const tool_1 = await import('../mcp/tools/ui-automation/gesture.ts').then((m) => m.default); + const tool_2 = await import('../mcp/tools/ui-automation/key_press.ts').then((m) => m.default); + const tool_3 = await import('../mcp/tools/ui-automation/key_sequence.ts').then( + (m) => m.default, + ); + const tool_4 = await import('../mcp/tools/ui-automation/long_press.ts').then((m) => m.default); + const tool_5 = await import('../mcp/tools/ui-automation/screenshot.ts').then((m) => m.default); + const tool_6 = await import('../mcp/tools/ui-automation/snapshot_ui.ts').then((m) => m.default); + const tool_7 = await import('../mcp/tools/ui-automation/swipe.ts').then((m) => m.default); + const tool_8 = await import('../mcp/tools/ui-automation/tap.ts').then((m) => m.default); + const tool_9 = await import('../mcp/tools/ui-automation/touch.ts').then((m) => m.default); + const tool_10 = await import('../mcp/tools/ui-automation/type_text.ts').then((m) => m.default); return { workflow, button: tool_0, - describe_ui: tool_1, - gesture: tool_2, - key_press: tool_3, - key_sequence: tool_4, - long_press: tool_5, - screenshot: tool_6, + gesture: tool_1, + key_press: tool_2, + key_sequence: tool_3, + long_press: tool_4, + screenshot: tool_5, + snapshot_ui: tool_6, swipe: tool_7, tap: tool_8, touch: tool_9, @@ -358,6 +360,17 @@ export const WORKFLOW_LOADERS = { clean: tool_0, }; }, + 'workflow-discovery': async () => { + const { workflow } = await import('../mcp/tools/workflow-discovery/index.ts'); + const tool_0 = await import('../mcp/tools/workflow-discovery/manage_workflows.ts').then( + (m) => m.default, + ); + + return { + workflow, + manage_workflows: tool_0, + }; + }, }; export type WorkflowName = keyof typeof WORKFLOW_LOADERS; @@ -419,8 +432,8 @@ export const WORKFLOW_METADATA = { description: 'Swift Package Manager operations for building, testing, running, and managing Swift packages and dependencies. Complete SPM workflow support.', }, - 'ui-testing': { - name: 'UI Testing & Automation', + 'ui-automation': { + name: 'UI Automation', description: 'UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows.', }, @@ -429,4 +442,8 @@ export const WORKFLOW_METADATA = { description: 'Essential project maintenance utilities for cleaning and managing existing projects. Provides clean operations for both .xcodeproj and .xcworkspace files.', }, + 'workflow-discovery': { + name: 'Workflow Discovery', + description: 'Manage the workflows that are enabled and disabled.', + }, }; diff --git a/src/core/plugin-registry.ts b/src/core/plugin-registry.ts index cf4b2aaa..b4224157 100644 --- a/src/core/plugin-registry.ts +++ b/src/core/plugin-registry.ts @@ -84,6 +84,10 @@ async function loadWorkflowTools(workflowModule: Record): Promi return tools; } +export function listWorkflowDirectoryNames(): string[] { + return Object.keys(WORKFLOW_LOADERS); +} + /** * Get workflow metadata by directory name using generated loaders */ diff --git a/src/core/plugin-types.ts b/src/core/plugin-types.ts index 4077682a..3f2beecb 100644 --- a/src/core/plugin-types.ts +++ b/src/core/plugin-types.ts @@ -23,4 +23,6 @@ export interface WorkflowGroup { readonly directoryName: string; } +export type WorkflowName = string; + export const defineTool = (meta: PluginMeta): PluginMeta => meta; diff --git a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts index ee83119b..bca7363c 100644 --- a/src/mcp/tools/device/__tests__/get_device_app_path.test.ts +++ b/src/mcp/tools/device/__tests__/get_device_app_path.test.ts @@ -24,9 +24,7 @@ describe('get_device_app_path plugin', () => { }); it('should have correct description', () => { - expect(getDeviceAppPath.description).toBe( - 'Retrieves the built app path for a connected device.', - ); + expect(getDeviceAppPath.description).toBe('Get device built app path.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/device/__tests__/install_app_device.test.ts b/src/mcp/tools/device/__tests__/install_app_device.test.ts index 856fd770..cccad73d 100644 --- a/src/mcp/tools/device/__tests__/install_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/install_app_device.test.ts @@ -32,7 +32,7 @@ describe('install_app_device plugin', () => { }); it('should have correct description', () => { - expect(installAppDevice.description).toBe('Installs an app on a connected device.'); + expect(installAppDevice.description).toBe('Install app on device.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/device/__tests__/launch_app_device.test.ts b/src/mcp/tools/device/__tests__/launch_app_device.test.ts index b87661da..1606c6c7 100644 --- a/src/mcp/tools/device/__tests__/launch_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/launch_app_device.test.ts @@ -27,7 +27,7 @@ describe('launch_app_device plugin (device-shared)', () => { }); it('should have correct description', () => { - expect(launchAppDevice.description).toBe('Launches an app on a connected device.'); + expect(launchAppDevice.description).toBe('Launch app on device.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/device/__tests__/list_devices.test.ts b/src/mcp/tools/device/__tests__/list_devices.test.ts index cbe576d6..de8f9fcc 100644 --- a/src/mcp/tools/device/__tests__/list_devices.test.ts +++ b/src/mcp/tools/device/__tests__/list_devices.test.ts @@ -26,9 +26,7 @@ describe('list_devices plugin (device-shared)', () => { }); it('should have correct description', () => { - expect(listDevices.description).toBe( - 'Lists connected physical Apple devices (iPhone, iPad, Apple Watch, Apple TV, Apple Vision Pro) with their UUIDs, names, and connection status. Use this to discover physical devices for testing.', - ); + expect(listDevices.description).toBe('List connected devices.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/device/__tests__/stop_app_device.test.ts b/src/mcp/tools/device/__tests__/stop_app_device.test.ts index 32d9ed96..cfa32bef 100644 --- a/src/mcp/tools/device/__tests__/stop_app_device.test.ts +++ b/src/mcp/tools/device/__tests__/stop_app_device.test.ts @@ -21,7 +21,7 @@ describe('stop_app_device plugin', () => { }); it('should have correct description', () => { - expect(stopAppDevice.description).toBe('Stops a running app on a connected device.'); + expect(stopAppDevice.description).toBe('Stop device app.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/doctor/__tests__/doctor.test.ts b/src/mcp/tools/doctor/__tests__/doctor.test.ts index c7e38625..852787c4 100644 --- a/src/mcp/tools/doctor/__tests__/doctor.test.ts +++ b/src/mcp/tools/doctor/__tests__/doctor.test.ts @@ -88,10 +88,8 @@ function createDeps(overrides?: Partial): DoctorDependencies runtime: { async getRuntimeToolInfo() { return { - mode: 'runtime' as const, enabledWorkflows: ['doctor'], - enabledTools: ['doctor'], - totalRegistered: 1, + registeredToolCount: 1, }; }, }, @@ -132,9 +130,7 @@ describe('doctor tool', () => { }); it('should have correct description', () => { - expect(doctor.description).toBe( - 'Provides comprehensive information about the MCP server environment, available dependencies, and configuration status.', - ); + expect(doctor.description).toBe('MCP environment info.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/doctor/doctor.ts b/src/mcp/tools/doctor/doctor.ts index 74d080a3..09b4b3d2 100644 --- a/src/mcp/tools/doctor/doctor.ts +++ b/src/mcp/tools/doctor/doctor.ts @@ -58,6 +58,11 @@ export async function runDoctor( const axeAvailable = deps.features.areAxeToolsAvailable(); const pluginSystemInfo = await deps.plugins.getPluginSystemInfo(); const runtimeInfo = await deps.runtime.getRuntimeToolInfo(); + const runtimeRegistration = runtimeInfo ?? { + enabledWorkflows: [], + registeredToolCount: 0, + }; + const runtimeNote = runtimeInfo ? null : 'Runtime registry unavailable.'; const xcodemakeEnabled = deps.features.isXcodemakeEnabled(); const xcodemakeAvailable = await deps.features.isXcodemakeAvailable(); const makefileExists = deps.features.doesMakefileExist('./'); @@ -234,12 +239,11 @@ export async function runDoctor( : ['- Plugin directory grouping unavailable in this build']), `\n### Runtime Tool Registration`, - `- Mode: ${runtimeInfo.mode}`, - `- Enabled Workflows: ${runtimeInfo.enabledWorkflows.length}`, - `- Registered Tools: ${runtimeInfo.totalRegistered}`, - ...(runtimeInfo.mode === 'static' ? [`- Note: ${runtimeInfo.note}`] : []), - ...(runtimeInfo.enabledWorkflows.length > 0 - ? [`- Workflows: ${runtimeInfo.enabledWorkflows.join(', ')}`] + `- Enabled Workflows: ${runtimeRegistration.enabledWorkflows.length}`, + `- Registered Tools: ${runtimeRegistration.registeredToolCount}`, + ...(runtimeNote ? [`- Note: ${runtimeNote}`] : []), + ...(runtimeRegistration.enabledWorkflows.length > 0 + ? [`- Workflows: ${runtimeRegistration.enabledWorkflows.join(', ')}`] : []), `\n## Tool Availability Summary`, diff --git a/src/mcp/tools/doctor/lib/doctor.deps.ts b/src/mcp/tools/doctor/lib/doctor.deps.ts index d02f2cd4..110f8c4e 100644 --- a/src/mcp/tools/doctor/lib/doctor.deps.ts +++ b/src/mcp/tools/doctor/lib/doctor.deps.ts @@ -1,11 +1,8 @@ import * as os from 'os'; import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { loadWorkflowGroups } from '../../../../utils/plugin-registry/index.ts'; -import { getRuntimeRegistration } from '../../../../utils/runtime-registry.ts'; -import { - collectToolNames, - resolveSelectedWorkflows, -} from '../../../../utils/workflow-selection.ts'; +import type { RuntimeToolInfo } from '../../../../utils/tool-registry.ts'; +import { getRuntimeRegistration } from '../../../../utils/tool-registry.ts'; import { areAxeToolsAvailable, resolveAxeBinary } from '../../../../utils/axe/index.ts'; import { isXcodemakeEnabled, @@ -62,21 +59,7 @@ export interface PluginInfoProvider { } export interface RuntimeInfoProvider { - getRuntimeToolInfo(): Promise< - | { - mode: 'runtime'; - enabledWorkflows: string[]; - enabledTools: string[]; - totalRegistered: number; - } - | { - mode: 'static'; - enabledWorkflows: string[]; - enabledTools: string[]; - totalRegistered: number; - note: string; - } - >; + getRuntimeToolInfo(): Promise; } export interface FeatureDetector { @@ -194,11 +177,11 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen envVars[varName] = process.env[varName]; } - Object.keys(process.env).forEach((key) => { + for (const key of Object.keys(process.env)) { if (key.startsWith('XCODEBUILDMCP_')) { envVars[key] = process.env[key]; } - }); + } return envVars; }, @@ -261,29 +244,7 @@ export function createDoctorDependencies(executor: CommandExecutor): DoctorDepen const runtime: RuntimeInfoProvider = { async getRuntimeToolInfo() { - const runtimeInfo = getRuntimeRegistration(); - if (runtimeInfo) { - return runtimeInfo; - } - - const workflows = await loadWorkflowGroups(); - const enabledWorkflowEnv = process.env.XCODEBUILDMCP_ENABLED_WORKFLOWS ?? ''; - const workflowNames = enabledWorkflowEnv - .split(',') - .map((workflow) => workflow.trim()) - .filter(Boolean); - const selection = resolveSelectedWorkflows(workflows, workflowNames); - const enabledWorkflows = selection.selectedWorkflows.map( - (workflow) => workflow.directoryName, - ); - const enabledTools = collectToolNames(selection.selectedWorkflows); - return { - mode: 'static', - enabledWorkflows, - enabledTools, - totalRegistered: enabledTools.length, - note: 'Runtime registry unavailable; showing expected tools from selection rules.', - }; + return getRuntimeRegistration(); }, }; 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 d8574ad0..3b746075 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 @@ -32,9 +32,7 @@ describe('stop_device_log_cap plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe( - 'Stops an active Apple device log capture session and returns the captured logs.', - ); + expect(plugin.description).toBe('Stop device log capture.'); }); it('should have correct schema structure', () => { diff --git a/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts b/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts index 34f925f3..f7962487 100644 --- a/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts +++ b/src/mcp/tools/logging/__tests__/stop_sim_log_cap.test.ts @@ -31,9 +31,7 @@ describe('stop_sim_log_cap plugin', () => { expect(stopSimLogCap).toHaveProperty('handler'); expect(stopSimLogCap.name).toBe('stop_sim_log_cap'); - expect(stopSimLogCap.description).toBe( - 'Stops an active simulator log capture session and returns the captured logs.', - ); + expect(stopSimLogCap.description).toBe('Stop sim log capture.'); expect(typeof stopSimLogCap.handler).toBe('function'); expect(typeof stopSimLogCap.schema).toBe('object'); }); diff --git a/src/mcp/tools/macos/__tests__/build_macos.test.ts b/src/mcp/tools/macos/__tests__/build_macos.test.ts index 2a343aea..dbd1ea7b 100644 --- a/src/mcp/tools/macos/__tests__/build_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_macos.test.ts @@ -22,7 +22,7 @@ describe('build_macos plugin', () => { }); it('should have correct description', () => { - expect(buildMacOS.description).toBe('Builds a macOS app.'); + expect(buildMacOS.description).toBe('Build macOS app.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts index cc384812..679277d9 100644 --- a/src/mcp/tools/macos/__tests__/build_run_macos.test.ts +++ b/src/mcp/tools/macos/__tests__/build_run_macos.test.ts @@ -15,7 +15,7 @@ describe('build_run_macos', () => { }); it('should export the correct description', () => { - expect(tool.description).toBe('Builds and runs a macOS app.'); + expect(tool.description).toBe('Build and run macOS app.'); }); it('should export a handler function', () => { diff --git a/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts b/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts index b6eeedc5..59cba47a 100644 --- a/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/launch_mac_app.test.ts @@ -22,9 +22,7 @@ describe('launch_mac_app plugin', () => { }); it('should have correct description', () => { - expect(launchMacApp.description).toBe( - "Launches a macOS application. IMPORTANT: You MUST provide the appPath parameter. Example: launch_mac_app({ appPath: '/path/to/your/app.app' }) Note: In some environments, this tool may be prefixed as mcp0_launch_macos_app.", - ); + expect(launchMacApp.description).toBe('Launch macOS app.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts index b512ff99..2ab03f94 100644 --- a/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts +++ b/src/mcp/tools/macos/__tests__/stop_mac_app.test.ts @@ -20,9 +20,7 @@ describe('stop_mac_app plugin', () => { }); it('should have correct description', () => { - expect(stopMacApp.description).toBe( - 'Stops a running macOS application. Can stop by app name or process ID.', - ); + expect(stopMacApp.description).toBe('Stop macOS app.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts b/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts index d15cbf59..deb24914 100644 --- a/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts +++ b/src/mcp/tools/project-discovery/__tests__/list_schemes.test.ts @@ -24,7 +24,7 @@ describe('list_schemes plugin', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe('Lists schemes for a project or workspace.'); + expect(plugin.description).toBe('List Xcode schemes.'); }); it('should have handler function', () => { 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 65451c6d..49adea3a 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 @@ -90,9 +90,7 @@ describe('scaffold_macos_project plugin', () => { }); it('should have correct description field', () => { - expect(plugin.description).toBe( - 'Scaffold a new macOS project from templates. Creates a modern Xcode project with workspace structure, SPM package for features, and proper macOS configuration.', - ); + expect(plugin.description).toBe('Scaffold macOS project.'); }); it('should have handler as function', () => { diff --git a/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts index 7d4a06df..d94d57c5 100644 --- a/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_clear_defaults.test.ts @@ -25,7 +25,7 @@ describe('session-clear-defaults tool', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe('Clear selected or all session defaults.'); + expect(plugin.description).toBe('Clear session defaults.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts index f7cb9b9b..3e78fcda 100644 --- a/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_set_defaults.test.ts @@ -14,7 +14,7 @@ describe('session-set-defaults tool', () => { it('should have correct description', () => { expect(plugin.description).toBe( - 'Set the session defaults needed by many tools. Most tools require one or more session defaults to be set before they can be used. Agents should set all relevant defaults up front in a single call (e.g., project/workspace, scheme, simulator or device ID, useLatestOS) to avoid iterative prompts; only set the keys your workflow needs.', + 'Set the session defaults, should be called at least once to set tool defaults.', ); }); diff --git a/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts b/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts index 6096f4d6..9e182b2d 100644 --- a/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts +++ b/src/mcp/tools/session-management/__tests__/session_show_defaults.test.ts @@ -17,7 +17,7 @@ describe('session-show-defaults tool', () => { }); it('should have correct description', () => { - expect(plugin.description).toBe('Show current session defaults.'); + expect(plugin.description).toBe('Show session defaults.'); }); it('should have handler function', () => { diff --git a/src/mcp/tools/simulator/__tests__/boot_sim.test.ts b/src/mcp/tools/simulator/__tests__/boot_sim.test.ts index 070890e6..7f101a5a 100644 --- a/src/mcp/tools/simulator/__tests__/boot_sim.test.ts +++ b/src/mcp/tools/simulator/__tests__/boot_sim.test.ts @@ -23,7 +23,7 @@ describe('boot_sim tool', () => { }); it('should have concise description', () => { - expect(bootSim.description).toBe('Boots an iOS simulator.'); + expect(bootSim.description).toBe('Boot iOS simulator.'); }); it('should expose empty public schema', () => { diff --git a/src/mcp/tools/simulator/__tests__/screenshot.test.ts b/src/mcp/tools/simulator/__tests__/screenshot.test.ts index be01bc7c..6817c171 100644 --- a/src/mcp/tools/simulator/__tests__/screenshot.test.ts +++ b/src/mcp/tools/simulator/__tests__/screenshot.test.ts @@ -14,7 +14,7 @@ import { import type { CommandExecutor } from '../../../../utils/execution/index.ts'; import { SystemError } from '../../../../utils/responses/index.ts'; import { sessionStore } from '../../../../utils/session-store.ts'; -import screenshotPlugin, { screenshotLogic } from '../../ui-testing/screenshot.ts'; +import screenshotPlugin, { screenshotLogic } from '../../ui-automation/screenshot.ts'; describe('screenshot plugin', () => { beforeEach(() => { diff --git a/src/mcp/tools/simulator/describe_ui.ts b/src/mcp/tools/simulator/describe_ui.ts deleted file mode 100644 index 3208a22d..00000000 --- a/src/mcp/tools/simulator/describe_ui.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Re-export from ui-testing to avoid duplication -export { default } from '../ui-testing/describe_ui.ts'; diff --git a/src/mcp/tools/simulator/screenshot.ts b/src/mcp/tools/simulator/screenshot.ts index a4cde5a0..3eb979a5 100644 --- a/src/mcp/tools/simulator/screenshot.ts +++ b/src/mcp/tools/simulator/screenshot.ts @@ -1,2 +1,2 @@ // Re-export from ui-testing to avoid duplication -export { default } from '../ui-testing/screenshot.ts'; +export { default } from '../ui-automation/screenshot.ts'; diff --git a/src/mcp/tools/simulator/snapshot_ui.ts b/src/mcp/tools/simulator/snapshot_ui.ts new file mode 100644 index 00000000..04e4efb0 --- /dev/null +++ b/src/mcp/tools/simulator/snapshot_ui.ts @@ -0,0 +1,2 @@ +// Re-export from ui-automation to avoid duplication +export { default } from '../ui-automation/snapshot_ui.ts'; diff --git a/src/mcp/tools/ui-testing/__tests__/button.test.ts b/src/mcp/tools/ui-automation/__tests__/button.test.ts similarity index 100% rename from src/mcp/tools/ui-testing/__tests__/button.test.ts rename to src/mcp/tools/ui-automation/__tests__/button.test.ts diff --git a/src/mcp/tools/ui-testing/__tests__/gesture.test.ts b/src/mcp/tools/ui-automation/__tests__/gesture.test.ts similarity index 100% rename from src/mcp/tools/ui-testing/__tests__/gesture.test.ts rename to src/mcp/tools/ui-automation/__tests__/gesture.test.ts diff --git a/src/mcp/tools/ui-testing/__tests__/index.test.ts b/src/mcp/tools/ui-automation/__tests__/index.test.ts similarity index 90% rename from src/mcp/tools/ui-testing/__tests__/index.test.ts rename to src/mcp/tools/ui-automation/__tests__/index.test.ts index 288c3d58..e2a0e294 100644 --- a/src/mcp/tools/ui-testing/__tests__/index.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/index.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect } from 'vitest'; import { workflow } from '../index.ts'; -describe('ui-testing workflow metadata', () => { +describe('ui-automation workflow metadata', () => { describe('Workflow Structure', () => { it('should export workflow object with required properties', () => { expect(workflow).toHaveProperty('name'); @@ -12,7 +12,7 @@ describe('ui-testing workflow metadata', () => { }); it('should have correct workflow name', () => { - expect(workflow.name).toBe('UI Testing & Automation'); + expect(workflow.name).toBe('UI Automation'); }); it('should have correct description', () => { diff --git a/src/mcp/tools/ui-testing/__tests__/key_press.test.ts b/src/mcp/tools/ui-automation/__tests__/key_press.test.ts similarity index 100% rename from src/mcp/tools/ui-testing/__tests__/key_press.test.ts rename to src/mcp/tools/ui-automation/__tests__/key_press.test.ts diff --git a/src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts similarity index 100% rename from src/mcp/tools/ui-testing/__tests__/key_sequence.test.ts rename to src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts diff --git a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts b/src/mcp/tools/ui-automation/__tests__/long_press.test.ts similarity index 99% rename from src/mcp/tools/ui-testing/__tests__/long_press.test.ts rename to src/mcp/tools/ui-automation/__tests__/long_press.test.ts index 123a4c57..7185af1f 100644 --- a/src/mcp/tools/ui-testing/__tests__/long_press.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/long_press.test.ts @@ -329,7 +329,7 @@ describe('Long Press Plugin', () => { content: [ { type: 'text' as const, - text: 'Long press at (100, 200) for 1500ms simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Long press at (100, 200) for 1500ms simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, diff --git a/src/mcp/tools/ui-testing/__tests__/screenshot.test.ts b/src/mcp/tools/ui-automation/__tests__/screenshot.test.ts similarity index 100% rename from src/mcp/tools/ui-testing/__tests__/screenshot.test.ts rename to src/mcp/tools/ui-automation/__tests__/screenshot.test.ts diff --git a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts b/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts similarity index 91% rename from src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts rename to src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts index d97c0bb6..3ba5303b 100644 --- a/src/mcp/tools/ui-testing/__tests__/describe_ui.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/snapshot_ui.test.ts @@ -1,31 +1,31 @@ /** - * Tests for describe_ui tool plugin + * Tests for snapshot_ui tool plugin */ import { describe, it, expect } from 'vitest'; import * as z from 'zod'; import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts'; import type { CommandExecutor } from '../../../../utils/execution/index.ts'; -import describeUIPlugin, { describe_uiLogic } from '../describe_ui.ts'; +import snapshotUIPlugin, { snapshot_uiLogic } from '../snapshot_ui.ts'; -describe('Describe UI Plugin', () => { +describe('Snapshot UI Plugin', () => { describe('Export Field Validation (Literal)', () => { it('should have correct name', () => { - expect(describeUIPlugin.name).toBe('describe_ui'); + expect(snapshotUIPlugin.name).toBe('snapshot_ui'); }); it('should have correct description', () => { - expect(describeUIPlugin.description).toBe( + expect(snapshotUIPlugin.description).toBe( 'Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.', ); }); it('should have handler function', () => { - expect(typeof describeUIPlugin.handler).toBe('function'); + expect(typeof snapshotUIPlugin.handler).toBe('function'); }); it('should expose public schema without simulatorId field', () => { - const schema = z.object(describeUIPlugin.schema); + const schema = z.object(snapshotUIPlugin.schema); expect(schema.safeParse({}).success).toBe(true); @@ -37,7 +37,7 @@ describe('Describe UI Plugin', () => { describe('Handler Behavior (Complete Literal Returns)', () => { it('should surface session default requirement when simulatorId is missing', async () => { - const result = await describeUIPlugin.handler({}); + const result = await snapshotUIPlugin.handler({}); expect(result.isError).toBe(true); expect(result.content[0].text).toContain('Missing required session defaults'); @@ -46,7 +46,7 @@ describe('Describe UI Plugin', () => { it('should handle invalid simulatorId format via schema validation', async () => { // Test the actual handler with invalid UUID format - const result = await describeUIPlugin.handler({ + const result = await snapshotUIPlugin.handler({ simulatorId: 'invalid-uuid-format', }); @@ -55,7 +55,7 @@ describe('Describe UI Plugin', () => { expect(result.content[0].text).toContain('Invalid Simulator UUID format'); }); - it('should return success for valid describe_ui execution', async () => { + it('should return success for valid snapshot_ui execution', async () => { const uiHierarchy = '{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}'; @@ -83,7 +83,7 @@ describe('Describe UI Plugin', () => { return mockExecutor(...args); }; - const result = await describe_uiLogic( + const result = await snapshot_uiLogic( { simulatorId: '12345678-1234-4234-8234-123456789012', }, @@ -108,7 +108,7 @@ describe('Describe UI Plugin', () => { type: 'text' as const, text: `Next Steps: - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) -- Re-run describe_ui after layout changes +- Re-run snapshot_ui after layout changes - If a debugger is attached, ensure the app is running (not stopped on breakpoints) - Screenshots are for visual verification only`, }, @@ -132,7 +132,7 @@ describe('Describe UI Plugin', () => { }), }; - const result = await describe_uiLogic( + const result = await snapshot_uiLogic( { simulatorId: '12345678-1234-4234-8234-123456789012', }, @@ -169,7 +169,7 @@ describe('Describe UI Plugin', () => { }), }; - const result = await describe_uiLogic( + const result = await snapshot_uiLogic( { simulatorId: '12345678-1234-4234-8234-123456789012', }, @@ -201,7 +201,7 @@ describe('Describe UI Plugin', () => { }), }; - const result = await describe_uiLogic( + const result = await snapshot_uiLogic( { simulatorId: '12345678-1234-4234-8234-123456789012', }, @@ -235,7 +235,7 @@ describe('Describe UI Plugin', () => { }), }; - const result = await describe_uiLogic( + const result = await snapshot_uiLogic( { simulatorId: '12345678-1234-4234-8234-123456789012', }, @@ -269,7 +269,7 @@ describe('Describe UI Plugin', () => { }), }; - const result = await describe_uiLogic( + const result = await snapshot_uiLogic( { simulatorId: '12345678-1234-4234-8234-123456789012', }, diff --git a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts b/src/mcp/tools/ui-automation/__tests__/swipe.test.ts similarity index 98% rename from src/mcp/tools/ui-testing/__tests__/swipe.test.ts rename to src/mcp/tools/ui-automation/__tests__/swipe.test.ts index 97c4a4a3..53e541be 100644 --- a/src/mcp/tools/ui-testing/__tests__/swipe.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/swipe.test.ts @@ -370,7 +370,7 @@ describe('Swipe Plugin', () => { content: [ { type: 'text' as const, - text: 'Swipe from (100, 200) to (300, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Swipe from (100, 200) to (300, 400) simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -403,7 +403,7 @@ describe('Swipe Plugin', () => { content: [ { type: 'text' as const, - text: 'Swipe from (100, 200) to (300, 400) duration=1.5s simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Swipe from (100, 200) to (300, 400) duration=1.5s simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, diff --git a/src/mcp/tools/ui-testing/__tests__/tap.test.ts b/src/mcp/tools/ui-automation/__tests__/tap.test.ts similarity index 97% rename from src/mcp/tools/ui-testing/__tests__/tap.test.ts rename to src/mcp/tools/ui-automation/__tests__/tap.test.ts index a8d0cfac..6a40f90f 100644 --- a/src/mcp/tools/ui-testing/__tests__/tap.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/tap.test.ts @@ -490,14 +490,14 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Tap at (100, 200) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Tap at (100, 200) simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, }); }); - it('should return successful response with coordinate warning when describe_ui not called', async () => { + it('should return successful response with coordinate warning when snapshot_ui not called', async () => { const mockExecutor = createMockExecutor({ success: true, output: 'Tap completed', @@ -519,7 +519,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Tap at (150, 300) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Tap at (150, 300) simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -550,7 +550,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Tap at (250, 400) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Tap at (250, 400) simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -579,7 +579,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Tap at (0, 0) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Tap at (0, 0) simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -608,7 +608,7 @@ describe('Tap Plugin', () => { content: [ { type: 'text', - text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Tap at (1920, 1080) simulated successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, diff --git a/src/mcp/tools/ui-testing/__tests__/touch.test.ts b/src/mcp/tools/ui-automation/__tests__/touch.test.ts similarity index 97% rename from src/mcp/tools/ui-testing/__tests__/touch.test.ts rename to src/mcp/tools/ui-automation/__tests__/touch.test.ts index 78c3237a..5d0afed3 100644 --- a/src/mcp/tools/ui-testing/__tests__/touch.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/touch.test.ts @@ -435,7 +435,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -473,7 +473,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -534,7 +534,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Touch event (touch down) at (100, 200) executed successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -577,7 +577,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Touch event (touch up) at (100, 200) executed successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, @@ -621,7 +621,7 @@ describe('Touch Plugin', () => { content: [ { type: 'text', - text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.', + text: 'Touch event (touch down+up) at (100, 200) executed successfully.\n\nWarning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.', }, ], isError: false, diff --git a/src/mcp/tools/ui-testing/__tests__/type_text.test.ts b/src/mcp/tools/ui-automation/__tests__/type_text.test.ts similarity index 100% rename from src/mcp/tools/ui-testing/__tests__/type_text.test.ts rename to src/mcp/tools/ui-automation/__tests__/type_text.test.ts diff --git a/src/mcp/tools/ui-testing/button.ts b/src/mcp/tools/ui-automation/button.ts similarity index 100% rename from src/mcp/tools/ui-testing/button.ts rename to src/mcp/tools/ui-automation/button.ts diff --git a/src/mcp/tools/ui-testing/gesture.ts b/src/mcp/tools/ui-automation/gesture.ts similarity index 100% rename from src/mcp/tools/ui-testing/gesture.ts rename to src/mcp/tools/ui-automation/gesture.ts diff --git a/src/mcp/tools/ui-testing/index.ts b/src/mcp/tools/ui-automation/index.ts similarity index 85% rename from src/mcp/tools/ui-testing/index.ts rename to src/mcp/tools/ui-automation/index.ts index 0cfb4e9e..8b283529 100644 --- a/src/mcp/tools/ui-testing/index.ts +++ b/src/mcp/tools/ui-automation/index.ts @@ -1,5 +1,5 @@ export const workflow = { - name: 'UI Testing & Automation', + name: 'UI Automation', description: 'UI automation and accessibility testing tools for iOS simulators. Perform gestures, interactions, screenshots, and UI analysis for automated testing workflows.', }; diff --git a/src/mcp/tools/ui-testing/key_press.ts b/src/mcp/tools/ui-automation/key_press.ts similarity index 100% rename from src/mcp/tools/ui-testing/key_press.ts rename to src/mcp/tools/ui-automation/key_press.ts diff --git a/src/mcp/tools/ui-testing/key_sequence.ts b/src/mcp/tools/ui-automation/key_sequence.ts similarity index 100% rename from src/mcp/tools/ui-testing/key_sequence.ts rename to src/mcp/tools/ui-automation/key_sequence.ts diff --git a/src/mcp/tools/ui-testing/long_press.ts b/src/mcp/tools/ui-automation/long_press.ts similarity index 92% rename from src/mcp/tools/ui-testing/long_press.ts rename to src/mcp/tools/ui-automation/long_press.ts index bd7954ea..11dc7182 100644 --- a/src/mcp/tools/ui-testing/long_press.ts +++ b/src/mcp/tools/ui-automation/long_press.ts @@ -2,7 +2,7 @@ * UI Testing Plugin: Long Press * * Long press at specific coordinates for given duration (ms). - * Use describe_ui for precise coordinates (don't guess from screenshots). + * Use snapshot_ui for precise coordinates (don't guess from screenshots). */ import * as z from 'zod'; @@ -153,25 +153,25 @@ export default { }), }; -// Session tracking for describe_ui warnings +// Session tracking for snapshot_ui warnings interface DescribeUISession { timestamp: number; simulatorId: string; } -const describeUITimestamps = new Map(); -const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds +const snapshotUiTimestamps = new Map(); +const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds function getCoordinateWarning(simulatorId: string): string | null { - const session = describeUITimestamps.get(simulatorId); + const session = snapshotUiTimestamps.get(simulatorId); if (!session) { - return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; + return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; } const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) { + if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`; + return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; } return null; diff --git a/src/mcp/tools/ui-testing/screenshot.ts b/src/mcp/tools/ui-automation/screenshot.ts similarity index 100% rename from src/mcp/tools/ui-testing/screenshot.ts rename to src/mcp/tools/ui-automation/screenshot.ts diff --git a/src/mcp/tools/ui-testing/describe_ui.ts b/src/mcp/tools/ui-automation/snapshot_ui.ts similarity index 85% rename from src/mcp/tools/ui-testing/describe_ui.ts rename to src/mcp/tools/ui-automation/snapshot_ui.ts index b63241b7..040e05d4 100644 --- a/src/mcp/tools/ui-testing/describe_ui.ts +++ b/src/mcp/tools/ui-automation/snapshot_ui.ts @@ -19,12 +19,12 @@ import { } from '../../../utils/typed-tool-factory.ts'; // Define schema as ZodObject -const describeUiSchema = z.object({ +const snapshotUiSchema = z.object({ simulatorId: z.uuid({ message: 'Invalid Simulator UUID format' }), }); // Use z.infer for type safety -type DescribeUiParams = z.infer; +type SnapshotUiParams = z.infer; export interface AxeHelpers { getAxePath: () => string | null; @@ -34,21 +34,21 @@ export interface AxeHelpers { const LOG_PREFIX = '[AXe]'; -// Session tracking for describe_ui warnings (shared across UI tools) -const describeUITimestamps = new Map(); +// Session tracking for snapshot_ui warnings (shared across UI tools) +const snapshotUiTimestamps = new Map(); -function recordDescribeUICall(simulatorId: string): void { - describeUITimestamps.set(simulatorId, { +function recordSnapshotUICall(simulatorId: string): void { + snapshotUiTimestamps.set(simulatorId, { timestamp: Date.now(), simulatorId, }); } /** - * Core business logic for describe_ui functionality + * Core business logic for snapshot_ui functionality */ -export async function describe_uiLogic( - params: DescribeUiParams, +export async function snapshot_uiLogic( + params: SnapshotUiParams, executor: CommandExecutor, axeHelpers: AxeHelpers = { getAxePath, @@ -57,7 +57,7 @@ export async function describe_uiLogic( }, debuggerManager: DebuggerManager = getDefaultDebuggerManager(), ): Promise { - const toolName = 'describe_ui'; + const toolName = 'snapshot_ui'; const { simulatorId } = params; const commandArgs = ['describe-ui']; @@ -79,8 +79,8 @@ export async function describe_uiLogic( axeHelpers, ); - // Record the describe_ui call for warning system - recordDescribeUICall(simulatorId); + // Record the snapshot_ui call for warning system + recordSnapshotUICall(simulatorId); log('info', `${LOG_PREFIX}/${toolName}: Success for ${simulatorId}`); const response: ToolResponse = { @@ -94,7 +94,7 @@ export async function describe_uiLogic( type: 'text', text: `Next Steps: - Use frame coordinates for tap/swipe (center: x+width/2, y+height/2) -- Re-run describe_ui after layout changes +- Re-run snapshot_ui after layout changes - If a debugger is attached, ensure the app is running (not stopped on breakpoints) - Screenshots are for visual verification only`, }, @@ -126,25 +126,25 @@ export async function describe_uiLogic( } const publicSchemaObject = z.strictObject( - describeUiSchema.omit({ simulatorId: true } as const).shape, + snapshotUiSchema.omit({ simulatorId: true } as const).shape, ); export default { - name: 'describe_ui', + name: 'snapshot_ui', description: 'Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.', schema: getSessionAwareToolSchemaShape({ sessionAware: publicSchemaObject, - legacy: describeUiSchema, + legacy: snapshotUiSchema, }), annotations: { - title: 'Describe UI', + title: 'Snapshot UI', readOnlyHint: true, }, - handler: createSessionAwareTool({ - internalSchema: describeUiSchema as unknown as z.ZodType, - logicFunction: (params: DescribeUiParams, executor: CommandExecutor) => - describe_uiLogic(params, executor, { + handler: createSessionAwareTool({ + internalSchema: snapshotUiSchema as unknown as z.ZodType, + logicFunction: (params: SnapshotUiParams, executor: CommandExecutor) => + snapshot_uiLogic(params, executor, { getAxePath, getBundledAxeEnvironment, createAxeNotAvailableResponse, diff --git a/src/mcp/tools/ui-testing/swipe.ts b/src/mcp/tools/ui-automation/swipe.ts similarity index 93% rename from src/mcp/tools/ui-testing/swipe.ts rename to src/mcp/tools/ui-automation/swipe.ts index f9beda26..f332670d 100644 --- a/src/mcp/tools/ui-testing/swipe.ts +++ b/src/mcp/tools/ui-automation/swipe.ts @@ -170,25 +170,25 @@ export default { }), }; -// Session tracking for describe_ui warnings +// Session tracking for snapshot_ui warnings interface DescribeUISession { timestamp: number; simulatorId: string; } -const describeUITimestamps = new Map(); -const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds +const snapshotUiTimestamps = new Map(); +const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds function getCoordinateWarning(simulatorId: string): string | null { - const session = describeUITimestamps.get(simulatorId); + const session = snapshotUiTimestamps.get(simulatorId); if (!session) { - return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; + return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; } const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) { + if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`; + return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; } return null; diff --git a/src/mcp/tools/ui-testing/tap.ts b/src/mcp/tools/ui-automation/tap.ts similarity index 94% rename from src/mcp/tools/ui-testing/tap.ts rename to src/mcp/tools/ui-automation/tap.ts index 7104c9c0..061ec077 100644 --- a/src/mcp/tools/ui-testing/tap.ts +++ b/src/mcp/tools/ui-automation/tap.ts @@ -90,20 +90,20 @@ const publicSchemaObject = z.strictObject(baseTapSchema.omit({ simulatorId: true const LOG_PREFIX = '[AXe]'; -// Session tracking for describe_ui warnings (shared across UI tools) -const describeUITimestamps = new Map(); -const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds +// Session tracking for snapshot_ui warnings (shared across UI tools) +const snapshotUiTimestamps = new Map(); +const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds function getCoordinateWarning(simulatorId: string): string | null { - const session = describeUITimestamps.get(simulatorId); + const session = snapshotUiTimestamps.get(simulatorId); if (!session) { - return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; + return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; } const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) { + if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`; + return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; } return null; diff --git a/src/mcp/tools/ui-testing/touch.ts b/src/mcp/tools/ui-automation/touch.ts similarity index 92% rename from src/mcp/tools/ui-testing/touch.ts rename to src/mcp/tools/ui-automation/touch.ts index 374595aa..8f638120 100644 --- a/src/mcp/tools/ui-testing/touch.ts +++ b/src/mcp/tools/ui-automation/touch.ts @@ -2,7 +2,7 @@ * UI Testing Plugin: Touch * * Perform touch down/up events at specific coordinates. - * Use describe_ui for precise coordinates (don't guess from screenshots). + * Use snapshot_ui for precise coordinates (don't guess from screenshots). */ import * as z from 'zod'; @@ -147,25 +147,25 @@ export default { }), }; -// Session tracking for describe_ui warnings +// Session tracking for snapshot_ui warnings interface DescribeUISession { timestamp: number; simulatorId: string; } -const describeUITimestamps = new Map(); -const DESCRIBE_UI_WARNING_TIMEOUT = 60000; // 60 seconds +const snapshotUiTimestamps = new Map(); +const SNAPSHOT_UI_WARNING_TIMEOUT = 60000; // 60 seconds function getCoordinateWarning(simulatorId: string): string | null { - const session = describeUITimestamps.get(simulatorId); + const session = snapshotUiTimestamps.get(simulatorId); if (!session) { - return 'Warning: describe_ui has not been called yet. Consider using describe_ui for precise coordinates instead of guessing from screenshots.'; + return 'Warning: snapshot_ui has not been called yet. Consider using snapshot_ui for precise coordinates instead of guessing from screenshots.'; } const timeSinceDescribe = Date.now() - session.timestamp; - if (timeSinceDescribe > DESCRIBE_UI_WARNING_TIMEOUT) { + if (timeSinceDescribe > SNAPSHOT_UI_WARNING_TIMEOUT) { const secondsAgo = Math.round(timeSinceDescribe / 1000); - return `Warning: describe_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with describe_ui instead of using potentially stale coordinates.`; + return `Warning: snapshot_ui was last called ${secondsAgo} seconds ago. Consider refreshing UI coordinates with snapshot_ui instead of using potentially stale coordinates.`; } return null; diff --git a/src/mcp/tools/ui-testing/type_text.ts b/src/mcp/tools/ui-automation/type_text.ts similarity index 100% rename from src/mcp/tools/ui-testing/type_text.ts rename to src/mcp/tools/ui-automation/type_text.ts diff --git a/src/mcp/tools/workflow-discovery/__tests__/manage_workflows.test.ts b/src/mcp/tools/workflow-discovery/__tests__/manage_workflows.test.ts new file mode 100644 index 00000000..1aaeda12 --- /dev/null +++ b/src/mcp/tools/workflow-discovery/__tests__/manage_workflows.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +vi.mock('../../../../utils/tool-registry.ts', () => ({ + applyWorkflowSelection: vi.fn(), + getRegisteredWorkflows: vi.fn(), +})); + +import { manage_workflowsLogic } from '../manage_workflows.ts'; +import { createMockExecutor } from '../../../../test-utils/mock-executors.ts'; +import { applyWorkflowSelection, getRegisteredWorkflows } from '../../../../utils/tool-registry.ts'; + +describe('manage_workflows tool', () => { + beforeEach(() => { + vi.mocked(applyWorkflowSelection).mockReset(); + vi.mocked(getRegisteredWorkflows).mockReset(); + }); + + it('merges new workflows with current set when enable is true', async () => { + vi.mocked(getRegisteredWorkflows).mockReturnValue(['simulator']); + vi.mocked(applyWorkflowSelection).mockResolvedValue({ + enabledWorkflows: ['simulator', 'device'], + registeredToolCount: 0, + }); + + const executor = createMockExecutor({ success: true, output: '' }); + const result = await manage_workflowsLogic( + { workflowNames: ['device'], enable: true }, + executor, + ); + + expect(vi.mocked(applyWorkflowSelection)).toHaveBeenCalledWith(['simulator', 'device']); + expect(result.content[0].text).toBe('Workflows enabled: simulator, device'); + }); + + it('removes requested workflows when enable is false', async () => { + vi.mocked(getRegisteredWorkflows).mockReturnValue(['simulator', 'device']); + vi.mocked(applyWorkflowSelection).mockResolvedValue({ + enabledWorkflows: ['simulator'], + registeredToolCount: 0, + }); + + const executor = createMockExecutor({ success: true, output: '' }); + const result = await manage_workflowsLogic( + { workflowNames: ['device'], enable: false }, + executor, + ); + + expect(vi.mocked(applyWorkflowSelection)).toHaveBeenCalledWith(['simulator']); + expect(result.content[0].text).toBe('Workflows enabled: simulator'); + }); + + it('accepts workflowName as an array', async () => { + vi.mocked(getRegisteredWorkflows).mockReturnValue(['simulator']); + vi.mocked(applyWorkflowSelection).mockResolvedValue({ + enabledWorkflows: ['simulator', 'device', 'logging'], + registeredToolCount: 0, + }); + + const executor = createMockExecutor({ success: true, output: '' }); + await manage_workflowsLogic({ workflowNames: ['device', 'logging'], enable: true }, executor); + + expect(vi.mocked(applyWorkflowSelection)).toHaveBeenCalledWith([ + 'simulator', + 'device', + 'logging', + ]); + }); +}); diff --git a/src/mcp/tools/workflow-discovery/index.ts b/src/mcp/tools/workflow-discovery/index.ts new file mode 100644 index 00000000..3e4cd290 --- /dev/null +++ b/src/mcp/tools/workflow-discovery/index.ts @@ -0,0 +1,4 @@ +export const workflow = { + name: 'Workflow Discovery', + description: `Manage the workflows that are enabled and disabled.`, +}; diff --git a/src/mcp/tools/workflow-discovery/manage_workflows.ts b/src/mcp/tools/workflow-discovery/manage_workflows.ts new file mode 100644 index 00000000..aac443ad --- /dev/null +++ b/src/mcp/tools/workflow-discovery/manage_workflows.ts @@ -0,0 +1,49 @@ +import * as z from 'zod'; +import { nullifyEmptyStrings } from '../../../utils/schema-helpers.ts'; +import { createTypedTool } from '../../../utils/typed-tool-factory.ts'; +import { getDefaultCommandExecutor, type CommandExecutor } from '../../../utils/execution/index.ts'; +import { createTextResponse } from '../../../utils/responses/index.ts'; +import type { ToolResponse } from '../../../types/common.ts'; +import { applyWorkflowSelection, getRegisteredWorkflows } from '../../../utils/tool-registry.ts'; +import { listWorkflowDirectoryNames } from '../../../core/plugin-registry.ts'; + +const baseSchemaObject = z.object({ + workflowNames: z.array(z.string()).describe('Workflow directory name(s).'), + enable: z.boolean().describe('Enable or disable the selected workflows.'), +}); + +const manageWorkflowsSchema = z.preprocess(nullifyEmptyStrings, baseSchemaObject); + +export type ManageWorkflowsParams = z.infer; + +export async function manage_workflowsLogic( + params: ManageWorkflowsParams, + _neverExecutor: CommandExecutor, +): Promise { + const workflowNames = params.workflowNames; + const currentWorkflows = getRegisteredWorkflows(); + const requestedSet = new Set( + workflowNames.map((name) => name.trim().toLowerCase()).filter(Boolean), + ); + const nextWorkflows = + params.enable === false + ? currentWorkflows.filter((name) => !requestedSet.has(name.toLowerCase())) + : [...new Set([...currentWorkflows, ...workflowNames])]; + const registryState = await applyWorkflowSelection(nextWorkflows); + + return createTextResponse(`Workflows enabled: ${registryState.enabledWorkflows.join(', ')}`); +} + +const workflowNames = listWorkflowDirectoryNames(); +const availableWorkflows = + workflowNames.length > 0 ? workflowNames.join(', ') : 'none (no workflows discovered)'; + +export default { + name: 'manage-workflows', + description: `Workflows are groups of tools exposed by XcodeBuildMCP. +By default, not all workflows (and therefore tools) are enabled; only simulator tools are enabled by default. +Some workflows are mandatory and can't be disabled. +Available workflows: ${availableWorkflows}`, + schema: baseSchemaObject.shape, + handler: createTypedTool(manageWorkflowsSchema, manage_workflowsLogic, getDefaultCommandExecutor), +}; diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index f96302e4..2b352776 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -12,7 +12,7 @@ export interface BootstrapOptions { function parseEnabledWorkflows(value: string): string[] { return value .split(',') - .map((name) => name.trim()) + .map((name) => name.trim().toLowerCase()) .filter(Boolean); } @@ -37,10 +37,10 @@ export async function bootstrapServer( if (enabledWorkflows.length > 0) { log('info', `๐Ÿš€ Initializing server with selected workflows: ${enabledWorkflows.join(', ')}`); - await registerWorkflows(server, enabledWorkflows); + await registerWorkflows(enabledWorkflows); } else { log('info', '๐Ÿš€ Initializing server with all tools...'); - await registerWorkflows(server); + await registerWorkflows([]); } await registerResources(server); diff --git a/src/server/server-state.ts b/src/server/server-state.ts new file mode 100644 index 00000000..0b32d62e --- /dev/null +++ b/src/server/server-state.ts @@ -0,0 +1,13 @@ +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +let serverInstance: McpServer | undefined; + +export function getServer(): McpServer | undefined { + return serverInstance; +} + +export function setServer(server: McpServer): void { + serverInstance = server; +} + +export { serverInstance as server }; diff --git a/src/server/server.ts b/src/server/server.ts index a7f8bdd4..cabbb1dc 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -17,12 +17,16 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { log } from '../utils/logger.ts'; import { version } from '../version.ts'; import * as Sentry from '@sentry/node'; +import { getServer, setServer } from './server-state.ts'; /** * Create and configure the MCP server * @returns Configured MCP server instance */ export function createServer(): McpServer { + if (getServer()) { + throw new Error('MCP server already initialized.'); + } // Create server instance const baseServer = new McpServer( { @@ -44,12 +48,13 @@ export function createServer(): McpServer { ); // Wrap server with Sentry for MCP instrumentation - const server = Sentry.wrapMcpServerWithSentry(baseServer); + const wrappedServer = Sentry.wrapMcpServerWithSentry(baseServer); + setServer(wrappedServer); // Log server initialization log('info', `Server initialized with Sentry MCP instrumentation (version ${version})`); - return server; + return wrappedServer; } /** diff --git a/src/utils/__tests__/workflow-selection.test.ts b/src/utils/__tests__/workflow-selection.test.ts index c2a24668..f6df09e8 100644 --- a/src/utils/__tests__/workflow-selection.test.ts +++ b/src/utils/__tests__/workflow-selection.test.ts @@ -33,9 +33,11 @@ function makeWorkflowMap(names: string[]): Map { describe('resolveSelectedWorkflows', () => { let originalDebug: string | undefined; + let originalWorkflowDiscovery: string | undefined; beforeEach(() => { originalDebug = process.env.XCODEBUILDMCP_DEBUG; + originalWorkflowDiscovery = process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY; }); afterEach(() => { @@ -44,17 +46,34 @@ describe('resolveSelectedWorkflows', () => { } else { process.env.XCODEBUILDMCP_DEBUG = originalDebug; } + if (typeof originalWorkflowDiscovery === 'undefined') { + delete process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY; + } else { + process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY = originalWorkflowDiscovery; + } }); it('adds doctor when debug is enabled and selection list is provided', () => { process.env.XCODEBUILDMCP_DEBUG = 'true'; - const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']); + process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY = 'true'; + const workflows = makeWorkflowMap([ + 'session-management', + 'workflow-discovery', + 'doctor', + 'simulator', + ]); - const result = resolveSelectedWorkflows(workflows, ['simulator']); + const result = resolveSelectedWorkflows(['simulator'], workflows); - expect(result.selectedNames).toEqual(['session-management', 'doctor', 'simulator']); + expect(result.selectedNames).toEqual([ + 'session-management', + 'workflow-discovery', + 'doctor', + 'simulator', + ]); expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([ 'session-management', + 'workflow-discovery', 'doctor', 'simulator', ]); @@ -62,28 +81,56 @@ describe('resolveSelectedWorkflows', () => { it('does not add doctor when debug is disabled', () => { process.env.XCODEBUILDMCP_DEBUG = 'false'; - const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']); + process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY = 'true'; + const workflows = makeWorkflowMap([ + 'session-management', + 'workflow-discovery', + 'doctor', + 'simulator', + ]); - const result = resolveSelectedWorkflows(workflows, ['simulator']); + const result = resolveSelectedWorkflows(['simulator'], workflows); - expect(result.selectedNames).toEqual(['session-management', 'simulator']); + expect(result.selectedNames).toEqual(['session-management', 'workflow-discovery', 'simulator']); expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([ 'session-management', + 'workflow-discovery', 'simulator', ]); }); it('returns all workflows when no selection list is provided', () => { process.env.XCODEBUILDMCP_DEBUG = 'true'; - const workflows = makeWorkflowMap(['session-management', 'doctor', 'simulator']); + process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY = 'true'; + const workflows = makeWorkflowMap([ + 'session-management', + 'workflow-discovery', + 'doctor', + 'simulator', + ]); - const result = resolveSelectedWorkflows(workflows, []); + const result = resolveSelectedWorkflows([], workflows); expect(result.selectedNames).toBeNull(); expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([ 'session-management', + 'workflow-discovery', 'doctor', 'simulator', ]); }); + + it('excludes workflow-discovery when experimental flag is disabled', () => { + process.env.XCODEBUILDMCP_DEBUG = 'false'; + process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY = 'false'; + const workflows = makeWorkflowMap(['session-management', 'workflow-discovery', 'simulator']); + + const result = resolveSelectedWorkflows([], workflows); + + expect(result.selectedNames).toBeNull(); + expect(result.selectedWorkflows.map((workflow) => workflow.directoryName)).toEqual([ + 'session-management', + 'simulator', + ]); + }); }); diff --git a/src/utils/plugin-registry/index.ts b/src/utils/plugin-registry/index.ts index 3ab0f191..5a7f54fb 100644 --- a/src/utils/plugin-registry/index.ts +++ b/src/utils/plugin-registry/index.ts @@ -1 +1,5 @@ -export { loadWorkflowGroups, loadPlugins } from '../../core/plugin-registry.ts'; +export { + loadWorkflowGroups, + loadPlugins, + listWorkflowDirectoryNames, +} from '../../core/plugin-registry.ts'; diff --git a/src/utils/runtime-registry.ts b/src/utils/runtime-registry.ts deleted file mode 100644 index 54c80559..00000000 --- a/src/utils/runtime-registry.ts +++ /dev/null @@ -1,35 +0,0 @@ -export type RuntimeToolInfo = - | { - mode: 'runtime'; - enabledWorkflows: string[]; - enabledTools: string[]; - totalRegistered: number; - } - | { - mode: 'static'; - enabledWorkflows: string[]; - enabledTools: string[]; - totalRegistered: number; - note: string; - }; - -let runtimeToolInfo: RuntimeToolInfo | null = null; - -export function recordRuntimeRegistration(info: { - enabledWorkflows: string[]; - enabledTools: string[]; -}): void { - const enabledWorkflows = [...new Set(info.enabledWorkflows)]; - const enabledTools = [...new Set(info.enabledTools)]; - - runtimeToolInfo = { - mode: 'runtime', - enabledWorkflows, - enabledTools, - totalRegistered: enabledTools.length, - }; -} - -export function getRuntimeRegistration(): RuntimeToolInfo | null { - return runtimeToolInfo; -} diff --git a/src/utils/tool-registry.ts b/src/utils/tool-registry.ts index 188d70a7..796d3332 100644 --- a/src/utils/tool-registry.ts +++ b/src/utils/tool-registry.ts @@ -1,54 +1,91 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { loadWorkflowGroups } from '../core/plugin-registry.ts'; +import { type RegisteredTool } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { server } from '../server/server-state.ts'; import { ToolResponse } from '../types/common.ts'; import { log } from './logger.ts'; -import { recordRuntimeRegistration } from './runtime-registry.ts'; +import { loadWorkflowGroups } from '../core/plugin-registry.ts'; import { resolveSelectedWorkflows } from './workflow-selection.ts'; -/** - * Register workflows (selected list or all when omitted) - */ -export async function registerWorkflows( - server: McpServer, - workflowNames: string[] = [], -): Promise { +export interface RuntimeToolInfo { + enabledWorkflows: string[]; + registeredToolCount: number; +} + +const registryState: { + tools: Map; + enabledWorkflows: Set; +} = { + tools: new Map(), + enabledWorkflows: new Set(), +}; + +export function getRuntimeRegistration(): RuntimeToolInfo | null { + if (registryState.tools.size === 0 && registryState.enabledWorkflows.size === 0) { + return null; + } + return { + enabledWorkflows: [...registryState.enabledWorkflows], + registeredToolCount: registryState.tools.size, + }; +} + +export async function applyWorkflowSelection(workflowNames: string[]): Promise { + if (!server) { + throw new Error('Tool registry has not been initialized.'); + } + const workflowGroups = await loadWorkflowGroups(); - const selection = resolveSelectedWorkflows(workflowGroups, workflowNames); - let registeredCount = 0; - const registeredTools = new Set(); - const registeredWorkflows = new Set(); + const selection = resolveSelectedWorkflows(workflowNames, workflowGroups); + const desiredToolNames = new Set(); + const desiredWorkflows = new Set(); for (const workflow of selection.selectedWorkflows) { - registeredWorkflows.add(workflow.directoryName); + desiredWorkflows.add(workflow.directoryName); for (const tool of workflow.tools) { - if (registeredTools.has(tool.name)) { - continue; + desiredToolNames.add(tool.name); + if (!registryState.tools.has(tool.name)) { + const registeredTool = server.registerTool( + tool.name, + { + description: tool.description ?? '', + inputSchema: tool.schema, + annotations: tool.annotations, + }, + (args: unknown): Promise => tool.handler(args as Record), + ); + registryState.tools.set(tool.name, registeredTool); } - server.registerTool( - tool.name, - { - description: tool.description ?? '', - inputSchema: tool.schema, - annotations: tool.annotations, - }, - (args: unknown): Promise => tool.handler(args as Record), - ); - registeredTools.add(tool.name); - registeredCount += 1; } } - recordRuntimeRegistration({ - enabledWorkflows: [...registeredWorkflows], - enabledTools: [...registeredTools], - }); - - if (selection.selectedNames) { - log( - 'info', - `โœ… Registered ${registeredCount} tools from workflows: ${selection.selectedNames.join(', ')}`, - ); - } else { - log('info', `โœ… Registered ${registeredCount} tools in static mode.`); + for (const [toolName, registeredTool] of registryState.tools.entries()) { + if (!desiredToolNames.has(toolName)) { + registeredTool.remove(); + registryState.tools.delete(toolName); + } } + + registryState.enabledWorkflows = desiredWorkflows; + + const workflowLabel = selection.selectedNames?.join(', ') ?? 'all workflows'; + log('info', `โœ… Registered ${desiredToolNames.size} tools from workflows: ${workflowLabel}`); + + return { + enabledWorkflows: [...registryState.enabledWorkflows], + registeredToolCount: registryState.tools.size, + }; +} + +export function getRegisteredWorkflows(): string[] { + return [...registryState.enabledWorkflows]; +} + +/** + * Register workflows (selected list or all when omitted) + */ +export async function registerWorkflows(workflowNames?: string[]): Promise { + await applyWorkflowSelection(workflowNames ?? []); +} + +export async function updateWorkflows(workflowNames?: string[]): Promise { + await applyWorkflowSelection(workflowNames ?? []); } diff --git a/src/utils/workflow-selection.ts b/src/utils/workflow-selection.ts index e3b01e36..6baedc20 100644 --- a/src/utils/workflow-selection.ts +++ b/src/utils/workflow-selection.ts @@ -1,9 +1,12 @@ import type { WorkflowGroup } from '../core/plugin-types.ts'; -const REQUIRED_WORKFLOW = 'session-management'; -const DEBUG_WORKFLOW = 'doctor'; +export const REQUIRED_WORKFLOW = 'session-management'; +export const WORKFLOW_DISCOVERY_WORKFLOW = 'workflow-discovery'; +export const DEBUG_WORKFLOW = 'doctor'; -function normalizeWorkflowNames(workflowNames: string[]): string[] { +type WorkflowName = string; + +function normalizeWorkflowNames(workflowNames: WorkflowName[]): WorkflowName[] { return workflowNames.map((name) => name.trim().toLowerCase()).filter(Boolean); } @@ -11,28 +14,80 @@ function isWorkflowGroup(value: WorkflowGroup | undefined): value is WorkflowGro return Boolean(value); } -function isDebugEnabled(): boolean { +export function isDebugEnabled(): boolean { const value = process.env.XCODEBUILDMCP_DEBUG ?? ''; return value.toLowerCase() === 'true' || value === '1'; } -export function resolveSelectedWorkflows( - workflowGroups: Map, - workflowNames: string[] = [], +export function isWorkflowDiscoveryEnabled(): boolean { + const value = process.env.XCODEBUILDMCP_EXPERIMENTAL_WORKFLOW_DISCOVERY ?? ''; + return value.toLowerCase() === 'true' || value === '1'; +} + +/** + * Resolve selected workflow names to only include workflows that + * match real workflows, ensuring the mandatory workflows are always included. + * + * @param workflowNames - The list of selected workflow names + * @returns The list of workflows to register. + */ +export function resolveSelectedWorkflowNames( + workflowNames: WorkflowName[] = [], + availableWorkflowNames: WorkflowName[] = [], ): { - selectedWorkflows: WorkflowGroup[]; - selectedNames: string[] | null; + selectedWorkflowNames: WorkflowName[]; + selectedNames: WorkflowName[] | null; } { const normalizedNames = normalizeWorkflowNames(workflowNames); - const autoSelected = isDebugEnabled() ? [REQUIRED_WORKFLOW, DEBUG_WORKFLOW] : [REQUIRED_WORKFLOW]; + const baseAutoSelected = [REQUIRED_WORKFLOW]; + + if (isWorkflowDiscoveryEnabled()) { + baseAutoSelected.push(WORKFLOW_DISCOVERY_WORKFLOW); + } + + if (isDebugEnabled()) { + baseAutoSelected.push(DEBUG_WORKFLOW); + } + const selectedNames = - normalizedNames.length > 0 ? [...new Set([...autoSelected, ...normalizedNames])] : null; + normalizedNames.length > 0 ? [...new Set([...baseAutoSelected, ...normalizedNames])] : null; + + // Filter selected name to only include workflows that match real workflows + const selectedWorkflowNames = selectedNames + ? selectedNames.filter((workflowName) => availableWorkflowNames.includes(workflowName)) + : isWorkflowDiscoveryEnabled() + ? [...availableWorkflowNames] + : availableWorkflowNames.filter( + (workflowName) => workflowName !== WORKFLOW_DISCOVERY_WORKFLOW, + ); + + return { selectedWorkflowNames, selectedNames }; +} + +/** + * Resolve selected workflow groups to only include workflow groups that + * match real workflow groups, ensuring the mandatory workflow groups are always included. + * + * @param workflowNames - The list of selected workflow names + * @param workflowGroups - The map of workflow groups + * @returns The list of workflow groups to register. + */ +export function resolveSelectedWorkflows( + workflowNames: WorkflowName[] = [], + workflowGroupsParam?: Map, +): { + selectedWorkflows: WorkflowGroup[]; + selectedNames: WorkflowName[] | null; +} { + const resolvedWorkflowGroups = workflowGroupsParam ?? new Map(); + const availableWorkflowNames = [...resolvedWorkflowGroups.keys()]; + const selection = resolveSelectedWorkflowNames(workflowNames, availableWorkflowNames); - const selectedWorkflows = selectedNames - ? selectedNames.map((workflowName) => workflowGroups.get(workflowName)).filter(isWorkflowGroup) - : [...workflowGroups.values()]; + const selectedWorkflows = selection.selectedWorkflowNames + .map((workflowName) => resolvedWorkflowGroups.get(workflowName)) + .filter(isWorkflowGroup); - return { selectedWorkflows, selectedNames }; + return { selectedWorkflows, selectedNames: selection.selectedNames }; } export function collectToolNames(workflows: WorkflowGroup[]): string[] { diff --git a/tools.compact.json b/tools.compact.json index e5704d1b..4ea53b34 100644 --- a/tools.compact.json +++ b/tools.compact.json @@ -270,7 +270,7 @@ ] }, { - "name": "describe_ui", + "name": "snapshot_ui", "description": "Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.", "args": [] }, diff --git a/tools.new.json b/tools.new.json index 28b921cb..64997ed7 100644 --- a/tools.new.json +++ b/tools.new.json @@ -274,7 +274,7 @@ ] }, { - "name": "describe_ui", + "name": "snapshot_ui", "description": "Print view hierarchy with precise view coordinates (x, y, width, height) for visible elements.", "args": [] }, diff --git a/tools_original.json b/tools_original.json index d516859a..744d2311 100644 --- a/tools_original.json +++ b/tools_original.json @@ -270,7 +270,7 @@ ] }, { - "name": "describe_ui", + "name": "snapshot_ui", "description": "Gets entire view hierarchy with precise frame coordinates (x, y, width, height) for all visible elements. Use this before UI interactions or after layout changes - do NOT guess coordinates from screenshots. Returns JSON tree with frame data for accurate automation. Requires the target process to be running; paused debugger/breakpoints can yield an empty tree.", "args": [] }, @@ -522,7 +522,7 @@ }, { "name": "long_press", - "description": "Long press at specific coordinates for given duration (ms). Use describe_ui for precise coordinates (don't guess from screenshots).", + "description": "Long press at specific coordinates for given duration (ms). Use snapshot_ui for precise coordinates (don't guess from screenshots).", "args": [ { "name": "duration", @@ -660,7 +660,7 @@ }, { "name": "screenshot", - "description": "Captures screenshot for visual verification. For UI coordinates, use describe_ui instead (don't determine coordinates from screenshots).", + "description": "Captures screenshot for visual verification. For UI coordinates, use snapshot_ui instead (don't determine coordinates from screenshots).", "args": [] }, { @@ -1108,7 +1108,7 @@ }, { "name": "touch", - "description": "Perform touch down/up events at specific coordinates. Use describe_ui for precise coordinates (don't guess from screenshots).", + "description": "Perform touch down/up events at specific coordinates. Use snapshot_ui for precise coordinates (don't guess from screenshots).", "args": [ { "name": "delay", @@ -1134,7 +1134,7 @@ }, { "name": "type_text", - "description": "Type text (supports US keyboard characters). Use describe_ui to find text field, tap to focus, then type.", + "description": "Type text (supports US keyboard characters). Use snapshot_ui to find text field, tap to focus, then type.", "args": [ { "name": "text", From 736c51346284c591bc4272303585ea7ff1d2934c Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Mon, 26 Jan 2026 09:42:42 +0000 Subject: [PATCH 5/6] WIP --- .../workflow-discovery/manage_workflows.ts | 10 ++++--- src/utils/tool-registry.ts | 17 ++++++----- src/utils/workflow-selection.ts | 29 ++++++++++++------- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/src/mcp/tools/workflow-discovery/manage_workflows.ts b/src/mcp/tools/workflow-discovery/manage_workflows.ts index aac443ad..ee622d4c 100644 --- a/src/mcp/tools/workflow-discovery/manage_workflows.ts +++ b/src/mcp/tools/workflow-discovery/manage_workflows.ts @@ -25,10 +25,12 @@ export async function manage_workflowsLogic( const requestedSet = new Set( workflowNames.map((name) => name.trim().toLowerCase()).filter(Boolean), ); - const nextWorkflows = - params.enable === false - ? currentWorkflows.filter((name) => !requestedSet.has(name.toLowerCase())) - : [...new Set([...currentWorkflows, ...workflowNames])]; + let nextWorkflows: string[]; + if (params.enable === false) { + nextWorkflows = currentWorkflows.filter((name) => !requestedSet.has(name.toLowerCase())); + } else { + nextWorkflows = [...new Set([...currentWorkflows, ...workflowNames])]; + } const registryState = await applyWorkflowSelection(nextWorkflows); return createTextResponse(`Workflows enabled: ${registryState.enabledWorkflows.join(', ')}`); diff --git a/src/utils/tool-registry.ts b/src/utils/tool-registry.ts index 796d3332..7c597d32 100644 --- a/src/utils/tool-registry.ts +++ b/src/utils/tool-registry.ts @@ -41,18 +41,19 @@ export async function applyWorkflowSelection(workflowNames: string[]): Promise => tool.handler(args as Record), + (args: unknown): Promise => handler(args as Record), ); - registryState.tools.set(tool.name, registeredTool); + registryState.tools.set(name, registeredTool); } } } diff --git a/src/utils/workflow-selection.ts b/src/utils/workflow-selection.ts index 6baedc20..e9931db1 100644 --- a/src/utils/workflow-selection.ts +++ b/src/utils/workflow-selection.ts @@ -49,17 +49,24 @@ export function resolveSelectedWorkflowNames( baseAutoSelected.push(DEBUG_WORKFLOW); } - const selectedNames = - normalizedNames.length > 0 ? [...new Set([...baseAutoSelected, ...normalizedNames])] : null; - - // Filter selected name to only include workflows that match real workflows - const selectedWorkflowNames = selectedNames - ? selectedNames.filter((workflowName) => availableWorkflowNames.includes(workflowName)) - : isWorkflowDiscoveryEnabled() - ? [...availableWorkflowNames] - : availableWorkflowNames.filter( - (workflowName) => workflowName !== WORKFLOW_DISCOVERY_WORKFLOW, - ); + let selectedNames: WorkflowName[] | null = null; + if (normalizedNames.length > 0) { + selectedNames = [...new Set([...baseAutoSelected, ...normalizedNames])]; + } + + // Filter selected names to only include workflows that match real workflows. + let selectedWorkflowNames: WorkflowName[]; + if (selectedNames) { + selectedWorkflowNames = selectedNames.filter((workflowName) => + availableWorkflowNames.includes(workflowName), + ); + } else if (isWorkflowDiscoveryEnabled()) { + selectedWorkflowNames = [...availableWorkflowNames]; + } else { + selectedWorkflowNames = availableWorkflowNames.filter( + (workflowName) => workflowName !== WORKFLOW_DISCOVERY_WORKFLOW, + ); + } return { selectedWorkflowNames, selectedNames }; } From a4cb731e9d5edefaecafd4d23efd8496cdc8b2ba Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Mon, 26 Jan 2026 09:52:26 +0000 Subject: [PATCH 6/6] Remnove redundent workflow name --- src/core/plugin-types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/core/plugin-types.ts b/src/core/plugin-types.ts index 3f2beecb..4077682a 100644 --- a/src/core/plugin-types.ts +++ b/src/core/plugin-types.ts @@ -23,6 +23,4 @@ export interface WorkflowGroup { readonly directoryName: string; } -export type WorkflowName = string; - export const defineTool = (meta: PluginMeta): PluginMeta => meta;