ControlKit is the native XCTest runner used by the CLI's Apple-platform MCP
tools. The public runner project is
xcrs-controlkit. It uses
one Xcode project generated by XcodeGen, with platform-specific targets and
schemes:
| Platform | Scheme | App product |
|---|---|---|
| iOS / iPadOS | ControlKit-iOS |
ControlKit.app |
| tvOS | ControlKit-tvOS |
ControlKit.app |
| visionOS | ControlKit-visionOS |
ControlKit.app |
| watchOS | ControlKit-watchOS |
ControlKit.app |
| macOS | ControlKit-macOS |
ControlKit.app |
The scheme and target names keep their platform suffixes so Xcode can select
the correct SDK, while each built app is named ControlKit.
Install XcodeGen once, clone the runner project, and generate the Xcode project:
brew install xcodegen
cd <path-to-xcrs-controlkit>/Runners
xcodegen generateBuild the simulator runner for the platform you need:
xcodebuild -project XCRSControlKitRunner.xcodeproj \
-scheme ControlKit-<platform> \
-destination 'generic/platform=<platform> Simulator' \
build-for-testingReplace <platform> with iOS, tvOS, watchOS, or visionOS. macOS is
built with -destination 'platform=macOS' and runs on the local Mac rather
than a simulator.
Boot a compatible simulator and run the UI-test host:
xcodebuild -project XCRSControlKitRunner.xcodeproj \
-scheme ControlKit-<platform> \
-destination 'id=<simulator-udid>' \
test-without-buildingThe shared XCTest runner starts a local ControlKit JSON-RPC server on
127.0.0.1:12004. Keep that test process running while calling the MCP tools.
The macOS UI-test target includes the network-server entitlement required to
bind the local port.
The same runner also works installed on a real iOS, tvOS, watchOS, or
visionOS device, not just a simulator. Build and start it against the
device's UDID (find it with xcrun devicectl list devices) instead of a
simulator destination:
xcodebuild -project XCRSControlKitRunner.xcodeproj \
-scheme ControlKit-<platform> \
-destination 'id=<device-udid>' \
build-for-testing
TEST_RUNNER_CONTROLKIT_LISTEN_HOST=0.0.0.0 xcodebuild \
-project XCRSControlKitRunner.xcodeproj \
-scheme ControlKit-<platform> \
-destination 'id=<device-udid>' \
test-without-buildingThe CONTROLKIT_LISTEN_HOST environment variable controls what the
in-process RPC server binds to (Shared/ControlKitRPCServer.swift) — it
defaults to 127.0.0.1, which is loopback-only on the device itself and
unreachable from your Mac. xcodebuild forwards any TEST_RUNNER_-prefixed
environment variable into the test process with the prefix stripped, so
setting TEST_RUNNER_CONTROLKIT_LISTEN_HOST=0.0.0.0 makes the server bind on
every interface, including the one reachable over your LAN. Find the device's
address with its Bonjour hostname (<device-name>.local, discoverable via
dns-sd -B _airplay._tcp local for an Apple TV) or its CoreDevice tunnel
address (xcrun devicectl device info details --device <udid>, under
Tunnel IP Address).
Start either automation server:
xcrs --mcp
# or
smb --mcp --scope automationThe runner tools accept either simulator_name or simulator_udid for a
simulator, or host (plus controlkit_port, defaulting to 12004) for a
physical device or any other remote runner. Call device_select once to
remember the target so later tools need no target arguments. Both commands
expose the same unprefixed tool names.
| Tool | Runner | Purpose |
|---|---|---|
device_select |
All platforms | Remember the active simulator or device for later tools. |
device_capabilities |
All platforms | Read platform and capability information. |
input_click |
macOS | Click at screen coordinates. |
input_spatial_tap |
visionOS | Perform a spatial tap. |
input_tap |
iOS / tvOS / watchOS, simulator or physical | Perform a touch tap. |
input_text |
iOS / tvOS, simulator or physical | Type into the focused field. |
input_swipe |
iOS / tvOS, simulator or physical | Swipe between coordinates. |
input_button |
iOS / tvOS, simulator or physical | Press a Home or tvOS remote button. |
app_launch/app_terminate |
simulator or physical | Launch/terminate an app by bundle ID. On a physical device this calls the runner's device.apps.launch/device.apps.terminate RPC methods instead of simctl. |
screen_capture |
Simulator or physical | Capture a PNG screenshot. Uses simctl for a simulator, or devicectl device capture screenshot when a device identifier is given. |
ui_describe |
All UI-test runners | Read the accessibility hierarchy. |
ui_element_list |
All UI-test runners | Read only actionable elements with tap coordinates. |
For a local macOS runner, omit simulator fields and pass host and
controlkit_port when needed:
{
"host": "127.0.0.1",
"controlkit_port": 12004,
"x": 400,
"y": 300
}For a physical device, pass its Bonjour hostname or tunnel address as host
instead:
{
"host": "<device-name>.local",
"controlkit_port": 12004,
"button": "select"
}Platform-specific operations intentionally reject unsupported input. For example, visionOS rejects raw touch, watchOS rejects pointer and spatial input, and macOS rejects touch and Home-button requests.
For the product-level overview of end-to-end testing with xcrs and ControlKit, see smbcloud.xyz/xcrs-end-to-end-testing.