@smbcloud / cli / commits / 11f671a

Support physical devices in ControlKit MCP tools

smb_simulator_press_button, launch_app, terminate_app, and related tools required simulator_name/simulator_udid resolved via simctl, so they couldn't reach a ControlKit runner on a real iOS/tvOS device over the network. Add an optional host field (matching the pattern already used by smb_macos_click/smb_visionos_spatial_tap/smb_watchos_tap) and route app launch/terminate through the RPC methods instead of simctl when a host is given.

keypair34 committed Jul 31, 2026 at 00:10 UTC 11f671aed90bc552db0fadf64c877eb56dc88d8e
1 file changed +90 -8
crates/xcrs/src/mcp.rs
+90 -8
@@ -54,6 +54,9 @@ pub struct SimulatorAppArgs {
54 /// Simulator UDID.
55 #[serde(default)]
56 pub simulator_udid: Option<String>,
57 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
58 + #[serde(default)]
59 + pub host: Option<String>,
60 /// Local ControlKit JSON-RPC port. Defaults to 12004.
61 #[serde(default)]
62 pub controlkit_port: Option<u16>,
@@ -84,6 +87,9 @@ pub struct SimulatorTapArgs {
87 /// Simulator UDID.
88 #[serde(default)]
89 pub simulator_udid: Option<String>,
90 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
91 + #[serde(default)]
92 + pub host: Option<String>,
93 /// Local ControlKit JSON-RPC port. Defaults to 12004.
94 #[serde(default)]
95 pub controlkit_port: Option<u16>,
@@ -101,6 +107,9 @@ pub struct SimulatorTextArgs {
107 /// Simulator UDID.
108 #[serde(default)]
109 pub simulator_udid: Option<String>,
110 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
111 + #[serde(default)]
112 + pub host: Option<String>,
113 /// Local ControlKit JSON-RPC port. Defaults to 12004.
114 #[serde(default)]
115 pub controlkit_port: Option<u16>,
@@ -116,6 +125,9 @@ pub struct SimulatorSwipeArgs {
125 /// Simulator UDID.
126 #[serde(default)]
127 pub simulator_udid: Option<String>,
128 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
129 + #[serde(default)]
130 + pub host: Option<String>,
131 /// Local ControlKit JSON-RPC port. Defaults to 12004.
132 #[serde(default)]
133 pub controlkit_port: Option<u16>,
@@ -137,6 +149,9 @@ pub struct SimulatorOrientationArgs {
149 /// Simulator UDID.
150 #[serde(default)]
151 pub simulator_udid: Option<String>,
152 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
153 + #[serde(default)]
154 + pub host: Option<String>,
155 /// Local ControlKit JSON-RPC port. Defaults to 12004.
156 #[serde(default)]
157 pub controlkit_port: Option<u16>,
@@ -152,6 +167,9 @@ pub struct SimulatorControlKitArgs {
167 /// Simulator UDID.
168 #[serde(default)]
169 pub simulator_udid: Option<String>,
170 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
171 + #[serde(default)]
172 + pub host: Option<String>,
173 /// Local ControlKit JSON-RPC port. Defaults to 12004.
174 #[serde(default)]
175 pub controlkit_port: Option<u16>,
@@ -165,6 +183,9 @@ pub struct SimulatorButtonArgs {
183 /// Simulator UDID.
184 #[serde(default)]
185 pub simulator_udid: Option<String>,
186 + /// ControlKit host for a physical device or remote runner. Omit for a local simulator.
187 + #[serde(default)]
188 + pub host: Option<String>,
189 /// Local ControlKit JSON-RPC port. Defaults to 12004.
190 #[serde(default)]
191 pub controlkit_port: Option<u16>,
@@ -271,11 +292,18 @@ macro_rules! xcrs_mcp_tools {
292 fn controlkit_from_target(
293 simulator_name: &Option<String>,
294 simulator_udid: &Option<String>,
295 + host: &Option<String>,
296 controlkit_port: Option<u16>,
297 ) -> ::std::result::Result<
276 - ($crate::Simulator, $crate::ControlKit),
298 + (Option<$crate::Simulator>, $crate::ControlKit),
299 ::rmcp::model::ErrorData,
300 > {
301 + if let Some(host) = host {
302 + return Ok((
303 + None,
304 + $crate::ControlKit::with_host(host, controlkit_port.unwrap_or(12004)),
305 + ));
306 + }
307 let target = $crate::mcp::SimulatorTargetArgs {
308 simulator_name: simulator_name.clone(),
309 simulator_udid: simulator_udid.clone(),
@@ -283,11 +311,18 @@ macro_rules! xcrs_mcp_tools {
311 };
312 let simulator = Self::simulator_from_target(&target)?;
313 Ok((
286 - simulator,
314 + Some(simulator),
315 $crate::ControlKit::new(controlkit_port.unwrap_or(12004)),
316 ))
317 }
318
319 + fn target_label(simulator: &Option<$crate::Simulator>, host: &Option<String>) -> String {
320 + if let Some(simulator) = simulator {
321 + return simulator.name.clone();
322 + }
323 + host.clone().unwrap_or_else(|| "127.0.0.1".to_string())
324 + }
325 +
326 fn controlkit_from_endpoint(
327 endpoint: &$crate::mcp::ControlKitEndpointArgs,
328 ) -> ::std::result::Result<
@@ -600,6 +635,25 @@ macro_rules! xcrs_mcp_tools {
635 ::rmcp::model::CallToolResult,
636 ::rmcp::model::ErrorData,
637 > {
638 + if let Some(host) = &args.host {
639 + let controlkit =
640 + $crate::ControlKit::with_host(host, args.controlkit_port.unwrap_or(12004));
641 + controlkit
642 + .call(
643 + "device.apps.launch",
644 + ::serde_json::json!({ "bundleId": args.bundle_id }),
645 + )
646 + .await
647 + .map_err(|error| {
648 + ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
649 + })?;
650 + return Ok(::rmcp::model::CallToolResult::success(vec![
651 + ::rmcp::model::ContentBlock::text(format!(
652 + "Launched {} on {}.",
653 + args.bundle_id, host
654 + )),
655 + ]));
656 + }
657 let target = $crate::mcp::SimulatorTargetArgs {
658 simulator_name: args.simulator_name,
659 simulator_udid: args.simulator_udid,
@@ -635,6 +689,25 @@ macro_rules! xcrs_mcp_tools {
689 ::rmcp::model::CallToolResult,
690 ::rmcp::model::ErrorData,
691 > {
692 + if let Some(host) = &args.host {
693 + let controlkit =
694 + $crate::ControlKit::with_host(host, args.controlkit_port.unwrap_or(12004));
695 + controlkit
696 + .call(
697 + "device.apps.terminate",
698 + ::serde_json::json!({ "bundleId": args.bundle_id }),
699 + )
700 + .await
701 + .map_err(|error| {
702 + ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
703 + })?;
704 + return Ok(::rmcp::model::CallToolResult::success(vec![
705 + ::rmcp::model::ContentBlock::text(format!(
706 + "Terminated {} on {}.",
707 + args.bundle_id, host
708 + )),
709 + ]));
710 + }
711 let target = $crate::mcp::SimulatorTargetArgs {
712 simulator_name: args.simulator_name,
713 simulator_udid: args.simulator_udid,
@@ -708,6 +781,7 @@ macro_rules! xcrs_mcp_tools {
781 let (simulator, controlkit) = Self::controlkit_from_target(
782 &args.simulator_name,
783 &args.simulator_udid,
784 + &args.host,
785 args.controlkit_port,
786 )?;
787 let result = controlkit
@@ -742,6 +816,7 @@ macro_rules! xcrs_mcp_tools {
816 let (simulator, controlkit) = Self::controlkit_from_target(
817 &args.simulator_name,
818 &args.simulator_udid,
819 + &args.host,
820 args.controlkit_port,
821 )?;
822 let ui = controlkit
@@ -777,6 +852,7 @@ macro_rules! xcrs_mcp_tools {
852 let (simulator, controlkit) = Self::controlkit_from_target(
853 &args.simulator_name,
854 &args.simulator_udid,
855 + &args.host,
856 args.controlkit_port,
857 )?;
858 controlkit
@@ -791,7 +867,7 @@ macro_rules! xcrs_mcp_tools {
867 Ok(::rmcp::model::CallToolResult::success(vec![
868 ::rmcp::model::ContentBlock::text(format!(
869 "Tapped ({}, {}) on {}.",
794 - args.x, args.y, simulator.name
870 + args.x, args.y, Self::target_label(&simulator, &args.host)
871 )),
872 ]))
873 }
@@ -814,6 +890,7 @@ macro_rules! xcrs_mcp_tools {
890 let (simulator, controlkit) = Self::controlkit_from_target(
891 &args.simulator_name,
892 &args.simulator_udid,
893 + &args.host,
894 args.controlkit_port,
895 )?;
896 controlkit
@@ -825,7 +902,7 @@ macro_rules! xcrs_mcp_tools {
902 Ok(::rmcp::model::CallToolResult::success(vec![
903 ::rmcp::model::ContentBlock::text(format!(
904 "Typed text on {}.",
828 - simulator.name
905 + Self::target_label(&simulator, &args.host)
906 )),
907 ]))
908 }
@@ -848,6 +925,7 @@ macro_rules! xcrs_mcp_tools {
925 let (simulator, controlkit) = Self::controlkit_from_target(
926 &args.simulator_name,
927 &args.simulator_udid,
928 + &args.host,
929 args.controlkit_port,
930 )?;
931 controlkit
@@ -867,7 +945,7 @@ macro_rules! xcrs_mcp_tools {
945 Ok(::rmcp::model::CallToolResult::success(vec![
946 ::rmcp::model::ContentBlock::text(format!(
947 "Swiped on {}.",
870 - simulator.name
948 + Self::target_label(&simulator, &args.host)
949 )),
950 ]))
951 }
@@ -890,6 +968,7 @@ macro_rules! xcrs_mcp_tools {
968 let (simulator, controlkit) = Self::controlkit_from_target(
969 &args.simulator_name,
970 &args.simulator_udid,
971 + &args.host,
972 args.controlkit_port,
973 )?;
974 controlkit
@@ -901,7 +980,7 @@ macro_rules! xcrs_mcp_tools {
980 Ok(::rmcp::model::CallToolResult::success(vec![
981 ::rmcp::model::ContentBlock::text(format!(
982 "Pressed Home on {}.",
904 - simulator.name
983 + Self::target_label(&simulator, &args.host)
984 )),
985 ]))
986 }
@@ -924,6 +1003,7 @@ macro_rules! xcrs_mcp_tools {
1003 let (simulator, controlkit) = Self::controlkit_from_target(
1004 &args.simulator_name,
1005 &args.simulator_udid,
1006 + &args.host,
1007 args.controlkit_port,
1008 )?;
1009 let supported_buttons = [
@@ -954,7 +1034,7 @@ macro_rules! xcrs_mcp_tools {
1034 Ok(::rmcp::model::CallToolResult::success(vec![
1035 ::rmcp::model::ContentBlock::text(format!(
1036 "Pressed {} on {}.",
957 - args.button, simulator.name
1037 + args.button, Self::target_label(&simulator, &args.host)
1038 )),
1039 ]))
1040 }
@@ -977,6 +1057,7 @@ macro_rules! xcrs_mcp_tools {
1057 let (simulator, controlkit) = Self::controlkit_from_target(
1058 &args.simulator_name,
1059 &args.simulator_udid,
1060 + &args.host,
1061 args.controlkit_port,
1062 )?;
1063 let result = controlkit
@@ -1011,6 +1092,7 @@ macro_rules! xcrs_mcp_tools {
1092 let (simulator, controlkit) = Self::controlkit_from_target(
1093 &args.simulator_name,
1094 &args.simulator_udid,
1095 + &args.host,
1096 args.controlkit_port,
1097 )?;
1098 let orientation = args.orientation.to_uppercase();
@@ -1032,7 +1114,7 @@ macro_rules! xcrs_mcp_tools {
1114 Ok(::rmcp::model::CallToolResult::success(vec![
1115 ::rmcp::model::ContentBlock::text(format!(
1116 "Set orientation to {} on {}.",
1035 - orientation, simulator.name
1117 + orientation, Self::target_label(&simulator, &args.host)
1118 )),
1119 ]))
1120 }