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
}