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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [Unreleased]

### 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)).

## [2.3.2]

### Fixed
Expand Down
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,172 @@
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: {} });

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('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