Skip to Content
DeveloperCLIControlKit runners

ControlKit runners

Generated from docs/controlkit.md in smbcloud-cli . Edit it there.

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:

PlatformSchemeApp product
iOS / iPadOSControlKit-iOSControlKit.app
tvOSControlKit-tvOSControlKit.app
visionOSControlKit-visionOSControlKit.app
watchOSControlKit-watchOSControlKit.app
macOSControlKit-macOSControlKit.app

The scheme and target names keep their platform suffixes so Xcode can select the correct SDK, while each built app is named ControlKit.

Build a runner

Install XcodeGen once, clone the runner project, and generate the Xcode project:

brew install xcodegen cd <path-to-xcrs-controlkit>/Runners xcodegen generate

Build the simulator runner for the platform you need:

xcodebuild -project XCRSControlKitRunner.xcodeproj \ -scheme ControlKit-<platform> \ -destination 'generic/platform=<platform> Simulator' \ build-for-testing

Replace <platform> with iOS, tvOS, watchOS, or visionOS. macOS is built with -destination 'platform=macOS' and runs on the local Mac rather than a simulator.

Start the runner

Boot a compatible simulator and run the UI-test host:

xcodebuild -project XCRSControlKitRunner.xcodeproj \ -scheme ControlKit-<platform> \ -destination 'id=<simulator-udid>' \ test-without-building

The 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.

Run on a physical device

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-building

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

Connect through the automation MCP profile

Start either automation server:

xcrs --mcp # or smb --mcp --scope automation

The 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.

ToolRunnerPurpose
device_selectAll platformsRemember the active simulator or device for later tools.
device_capabilitiesAll platformsRead platform and capability information.
input_clickmacOSClick at screen coordinates.
input_spatial_tapvisionOSPerform a spatial tap.
input_tapiOS / tvOS / watchOS, simulator or physicalPerform a touch tap.
input_textiOS / tvOS, simulator or physicalType into the focused field.
input_swipeiOS / tvOS, simulator or physicalSwipe between coordinates.
input_buttoniOS / tvOS, simulator or physicalPress a Home or tvOS remote button.
app_launch/app_terminatesimulator or physicalLaunch/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_captureSimulator or physicalCapture a PNG screenshot. Uses simctl for a simulator, or devicectl device capture screenshot when a device identifier is given.
ui_describeAll UI-test runnersRead the accessibility hierarchy.
ui_element_listAll UI-test runnersRead 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.

Last updated on