Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ xcuserdata/
.claude/
**/.claude/settings.local.json

# Worktrees
.worktrees/

# incremental builds
Makefile
buildServer.json
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Added

- Added `toggle_software_keyboard` tool to show or hide the iOS Simulator software keyboard ([#346](https://github.com/getsentry/XcodeBuildMCP/issues/346)).
- Added `toggle_connect_hardware_keyboard` tool to toggle the iOS Simulator hardware keyboard connection ([#346](https://github.com/getsentry/XcodeBuildMCP/issues/346)).
- Added `xcodebuildmcp upgrade` command to check for updates and upgrade in place. Supports `--check` (report-only) and `--yes`/`-y` (skip confirmation). Detects install method (Homebrew, npm-global, npx) and queries the appropriate channel source (`brew info`, `npm view`, or GitHub Releases) for the latest version. Non-interactive environments exit 1 when an auto-upgrade is possible but `--yes` was not supplied.

## [2.3.2]
Expand Down Expand Up @@ -433,4 +435,3 @@ Please note that the UI automation features are an early preview and currently i
- Initial release of XcodeBuildMCP
- Basic support for building iOS and macOS applications


12 changes: 7 additions & 5 deletions docs/TOOLS-CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This document lists CLI tool names as exposed by `xcodebuildmcp <workflow> <tool>`.

XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups.
XcodeBuildMCP provides 73 canonical tools organized into 13 workflow groups.

## Workflow Groups

Expand Down Expand Up @@ -128,7 +128,7 @@ XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups.


### 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. (8 tools)
**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. (10 tools)

- `boot` - Boot iOS simulator for manual/non-build flows. Not required before simulator build-and-run (build_run_sim).
- `erase` - Erase simulator.
Expand All @@ -138,6 +138,8 @@ XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups.
- `set-appearance` - Set sim appearance.
- `set-location` - Set sim location.
- `statusbar` - Set sim status bar network.
- `toggle-connect-hardware-keyboard` - Toggle whether the iOS Simulator receives Mac hardware keyboard input (Cmd+Shift+K). Disconnecting makes the on-screen keyboard appear for tap-based input. Requires the simulator to be booted and Accessibility permission for the MCP host.
- `toggle-software-keyboard` - Toggle the iOS Simulator software keyboard (Cmd+K). Shows or hides the on-screen keyboard. Requires the simulator to be booted and Accessibility permission for the MCP host.



Expand Down Expand Up @@ -185,10 +187,10 @@ XcodeBuildMCP provides 71 canonical tools organized into 13 workflow groups.

## Summary Statistics

- **Canonical Tools**: 71
- **Total Tools**: 99
- **Canonical Tools**: 73
- **Total Tools**: 101
- **Workflow Groups**: 13

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-04-07T11:23:03.868Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-04-19T09:35:05.443Z UTC*
12 changes: 7 additions & 5 deletions docs/TOOLS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# XcodeBuildMCP MCP Tools Reference

This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP provides 77 canonical tools organized into 15 workflow groups for comprehensive Apple development workflows.
This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP provides 79 canonical tools organized into 15 workflow groups for comprehensive Apple development workflows.

## Workflow Groups

Expand Down Expand Up @@ -137,7 +137,7 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov


### 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. (8 tools)
**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. (10 tools)

- `boot_sim` - Boot iOS simulator for manual/non-build flows. Not required before simulator build-and-run (build_run_sim).
- `erase_sims` - Erase simulator.
Expand All @@ -147,6 +147,8 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov
- `set_sim_appearance` - Set sim appearance.
- `set_sim_location` - Set sim location.
- `sim_statusbar` - Set sim status bar network.
- `toggle_connect_hardware_keyboard` - Toggle whether the iOS Simulator receives Mac hardware keyboard input (Cmd+Shift+K). Disconnecting makes the on-screen keyboard appear for tap-based input. Requires the simulator to be booted and Accessibility permission for the MCP host.
- `toggle_software_keyboard` - Toggle the iOS Simulator software keyboard (Cmd+K). Shows or hides the on-screen keyboard. Requires the simulator to be booted and Accessibility permission for the MCP host.



Expand Down Expand Up @@ -201,10 +203,10 @@ This document lists MCP tool names as exposed to MCP clients. XcodeBuildMCP prov

## Summary Statistics

- **Canonical Tools**: 77
- **Total Tools**: 105
- **Canonical Tools**: 79
- **Total Tools**: 107
- **Workflow Groups**: 15

---

*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-04-07T11:23:03.868Z UTC*
*This documentation is automatically generated by `scripts/update-tools-docs.ts` from the tools manifest. Last updated: 2026-04-19T09:35:05.443Z UTC*
12 changes: 12 additions & 0 deletions manifests/tools/toggle_connect_hardware_keyboard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
id: toggle_connect_hardware_keyboard
module: mcp/tools/simulator-management/toggle_connect_hardware_keyboard
names:
mcp: toggle_connect_hardware_keyboard
cli: toggle-connect-hardware-keyboard
description: Toggle whether the iOS Simulator receives Mac hardware keyboard input (Cmd+Shift+K). Disconnecting makes the on-screen keyboard appear for tap-based input. Requires the simulator to be booted and Accessibility permission for the MCP host.
annotations:
title: Toggle Connect Hardware Keyboard
readOnlyHint: false
destructiveHint: false
openWorldHint: false
idempotentHint: false
12 changes: 12 additions & 0 deletions manifests/tools/toggle_software_keyboard.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
id: toggle_software_keyboard
module: mcp/tools/simulator-management/toggle_software_keyboard
names:
mcp: toggle_software_keyboard
cli: toggle-software-keyboard
description: Toggle the iOS Simulator software keyboard (Cmd+K). Shows or hides the on-screen keyboard. Requires the simulator to be booted and Accessibility permission for the MCP host.
annotations:
title: Toggle Software Keyboard
readOnlyHint: false
destructiveHint: false
openWorldHint: false
idempotentHint: false
2 changes: 2 additions & 0 deletions manifests/workflows/simulator-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ tools:
- reset_sim_location
- set_sim_appearance
- sim_statusbar
- toggle_software_keyboard
- toggle_connect_hardware_keyboard
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { describe, it, expect } from 'vitest';
import {
createMockCommandResponse,
type CommandExecutor,
} from '../../../../test-utils/mock-executors.ts';
import { sendKeyboardShortcut } from '../_keyboard_shortcut.ts';

const BOOTED_JSON = JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
{ udid: 'test-uuid-123', name: 'iPhone 15 Pro', state: 'Booted' },
],
},
});

const SHUTDOWN_JSON = JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
{ udid: 'test-uuid-123', name: 'iPhone 15 Pro', state: 'Shutdown' },
],
},
});

const EMPTY_JSON = JSON.stringify({ devices: {} });
const ESCAPED_NAME_JSON = JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
{ udid: 'escaped-uuid', name: 'Test\\Device"', state: 'Booted' },
],
},
});
const PREFIX_NAME_JSON = JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
{ udid: 'prefix-uuid', name: 'iPhone 15', state: 'Booted' },
],
},
});

type Call = { command: string[] };

function makeFifoExecutor(
responses: Array<{ success: boolean; output?: string; error?: string }>,
): { executor: CommandExecutor; calls: Call[] } {
const calls: Call[] = [];
let i = 0;
const executor: CommandExecutor = async (command) => {
calls.push({ command });
const r = responses[i] ?? { success: true, output: '' };
i += 1;
return createMockCommandResponse({
success: r.success,
output: r.output ?? '',
error: r.error,
});
};
return { executor, calls };
}

describe('sendKeyboardShortcut', () => {
it('sends Cmd+K for software-keyboard when simulator is booted and window exists', async () => {
const { executor, calls } = makeFifoExecutor([
{ success: true, output: BOOTED_JSON },
{ success: true, output: '' },
{ success: true, output: 'OK' },
{ success: true, output: '' },
]);

const result = await sendKeyboardShortcut('test-uuid-123', 'software-keyboard', executor);

expect(result.success).toBe(true);
expect(calls[0].command).toEqual(['xcrun', 'simctl', 'list', 'devices', '--json']);
expect(calls[1].command).toEqual(['open', '-a', 'Simulator']);
expect(calls[2].command[0]).toBe('osascript');
expect(calls[2].command.join(' ')).toContain('iPhone 15 Pro');
expect(calls[3].command[0]).toBe('osascript');
const keystrokeScript = calls[3].command.join(' ');
expect(keystrokeScript).toContain('keystroke "k"');
expect(keystrokeScript).toContain('command down');
expect(keystrokeScript).not.toContain('shift down');
});

it('sends Cmd+Shift+K for connect-hardware-keyboard', async () => {
const { executor, calls } = makeFifoExecutor([
{ success: true, output: BOOTED_JSON },
{ success: true, output: '' },
{ success: true, output: 'OK' },
{ success: true, output: '' },
]);

const result = await sendKeyboardShortcut(
'test-uuid-123',
'connect-hardware-keyboard',
executor,
);

expect(result.success).toBe(true);
const keystrokeScript = calls[3].command.join(' ');
expect(keystrokeScript).toContain('keystroke "k"');
expect(keystrokeScript).toContain('command down');
expect(keystrokeScript).toContain('shift down');
});

it('escapes backslashes before embedding simulator names in the focus AppleScript', async () => {
const { executor, calls } = makeFifoExecutor([
{ success: true, output: ESCAPED_NAME_JSON },
{ success: true, output: '' },
{ success: true, output: 'OK' },
{ success: true, output: '' },
]);

const result = await sendKeyboardShortcut('escaped-uuid', 'software-keyboard', executor);

expect(result.success).toBe(true);
expect(calls[2].command[2]).toContain('Test\\\\Device\\"');
});

it('matches the simulator window by exact title or runtime suffix instead of substring contains', async () => {
const { executor, calls } = makeFifoExecutor([
{ success: true, output: PREFIX_NAME_JSON },
{ success: true, output: '' },
{ success: true, output: 'OK' },
{ success: true, output: '' },
]);

const result = await sendKeyboardShortcut('prefix-uuid', 'software-keyboard', executor);

expect(result.success).toBe(true);
expect(calls[2].command[2]).toContain('title is "iPhone 15"');
expect(calls[2].command[2]).toContain('title starts with "iPhone 15 –"');
expect(calls[2].command[2]).not.toContain('title contains');
});

it('errors when simulator UUID is not found', async () => {
const { executor, calls } = makeFifoExecutor([{ success: true, output: EMPTY_JSON }]);

const result = await sendKeyboardShortcut('missing-uuid', 'software-keyboard', executor);

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('missing-uuid');
expect(result.error).toContain('not found');
}
expect(calls).toHaveLength(1);
});

it('errors when simulator is not booted', async () => {
const { executor, calls } = makeFifoExecutor([{ success: true, output: SHUTDOWN_JSON }]);

const result = await sendKeyboardShortcut('test-uuid-123', 'software-keyboard', executor);

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('not booted');
}
expect(calls).toHaveLength(1);
});

it('errors when `open -a Simulator` fails', async () => {
const { executor, calls } = makeFifoExecutor([
{ success: true, output: BOOTED_JSON },
{ success: false, error: 'could not open' },
]);

const result = await sendKeyboardShortcut('test-uuid-123', 'software-keyboard', executor);

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('Simulator app');
}
expect(calls).toHaveLength(2);
});

it('errors and does not send keystroke when window lookup returns NO_WINDOW', async () => {
const { executor, calls } = makeFifoExecutor([
{ success: true, output: BOOTED_JSON },
{ success: true, output: '' },
{ success: true, output: 'NO_WINDOW' },
]);

const result = await sendKeyboardShortcut('test-uuid-123', 'software-keyboard', executor);

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('iPhone 15 Pro');
}
expect(calls).toHaveLength(3);
});

it('errors when simctl list fails', async () => {
const { executor } = makeFifoExecutor([{ success: false, error: 'simctl blew up' }]);

const result = await sendKeyboardShortcut('test-uuid-123', 'software-keyboard', executor);

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('simctl blew up');
}
});

it('errors when keystroke osascript fails', async () => {
const { executor } = makeFifoExecutor([
{ success: true, output: BOOTED_JSON },
{ success: true, output: '' },
{ success: true, output: 'OK' },
{ success: false, error: 'accessibility denied' },
]);

const result = await sendKeyboardShortcut('test-uuid-123', 'software-keyboard', executor);

expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toContain('accessibility denied');
}
});
});
Loading