7 files changed
+351
-25
crates/cli/src/cli/mod.rs
+4
-2
@@ -141,8 +141,10 @@ pub enum ControlKitCommands {
141
},
142
#[clap(about = "Call a running ControlKit JSON-RPC method.")]
143
Call {
144
- #[arg(long)]
145
- device_udid: String,
144
+ #[arg(long, required_unless_present = "host", conflicts_with = "host")]
145
+ device_udid: Option<String>,
146
+ #[arg(long, conflicts_with = "device_udid")]
147
+ host: Option<String>,
148
#[arg(long, default_value_t = 12004)]
149
port: u16,
150
#[arg(long)]
crates/cli/src/controlkit.rs
+13
-3
@@ -83,15 +83,25 @@ pub async fn process_controlkit(command: ControlKitCommands) -> Result<CommandRe
83
}
84
ControlKitCommands::Call {
85
device_udid,
86
+ host,
87
port,
88
method,
89
params,
90
} => {
90
- let tools = XcodeCommandLineTools::new();
91
- let tunnel_host = tools.device_tunnel_address(&device_udid)?;
91
+ let host = match (host, device_udid) {
92
+ (Some(host), None) => host,
93
+ (None, Some(device_udid)) => {
94
+ XcodeCommandLineTools::new().device_tunnel_address(&device_udid)?
95
+ }
96
+ _ => {
97
+ return Err(anyhow!(
98
+ "provide either --host or --device-udid for the ControlKit runner"
99
+ ));
100
+ }
101
+ };
102
let params = serde_json::from_str(¶ms)
103
.with_context(|| format!("invalid JSON for --params: {params}"))?;
94
- let result = ControlKit::with_host(&tunnel_host, port)
104
+ let result = ControlKit::with_host(&host, port)
105
.call(&method, params)
106
.await?;
107
Ok(done_result(&serde_json::to_string_pretty(&result)?))
crates/cli/src/mcp.rs
+2
-1
@@ -932,7 +932,8 @@ impl ServerHandler for SmbMcpServer {
932
"smbCloud CLI exposed as MCP tools. Authentication uses the token stored by \
933
`smb login`; tools run non-interactively. `tenant_use` / `project_use` select \
934
the tenant/project context other tools default to, shared with the CLI's own \
935
- `smb tenant use` / `smb project use`.",
935
+ `smb tenant use` / `smb project use`. ControlKit tools cover iOS, tvOS, \
936
+ visionOS, watchOS, and macOS runners.",
937
)
938
}
939
}
crates/cli/src/mcp_xcrs.rs
+5
-1
@@ -17,5 +17,9 @@ xcrs::xcrs_mcp_tools!(
17
"smb_simulator_press_home",
18
"smb_simulator_press_button",
19
"smb_simulator_orientation_get",
20
- "smb_simulator_orientation_set"
20
+ "smb_simulator_orientation_set",
21
+ "smb_controlkit_capabilities",
22
+ "smb_macos_click",
23
+ "smb_visionos_spatial_tap",
24
+ "smb_watchos_tap"
25
);
crates/xcrs/src/mcp.rs
+243
-15
@@ -172,6 +172,42 @@ pub struct SimulatorButtonArgs {
172
pub button: String,
173
}
174
175
+#[derive(Debug, Deserialize, JsonSchema)]
176
+pub struct ControlKitEndpointArgs {
177
+ /// Exact simulator name. Omit this when connecting to a local macOS runner.
178
+ #[serde(default)]
179
+ pub simulator_name: Option<String>,
180
+ /// Simulator UDID. Omit this when connecting to a local macOS runner.
181
+ #[serde(default)]
182
+ pub simulator_udid: Option<String>,
183
+ /// ControlKit host. Defaults to 127.0.0.1 for local runners.
184
+ #[serde(default)]
185
+ pub host: Option<String>,
186
+ /// Local ControlKit JSON-RPC port. Defaults to 12004.
187
+ #[serde(default)]
188
+ pub controlkit_port: Option<u16>,
189
+}
190
+
191
+#[derive(Debug, Deserialize, JsonSchema)]
192
+pub struct ControlKitCoordinateArgs {
193
+ /// Exact simulator name. Omit this when connecting to a local macOS runner.
194
+ #[serde(default)]
195
+ pub simulator_name: Option<String>,
196
+ /// Simulator UDID. Omit this when connecting to a local macOS runner.
197
+ #[serde(default)]
198
+ pub simulator_udid: Option<String>,
199
+ /// ControlKit host. Defaults to 127.0.0.1 for local runners.
200
+ #[serde(default)]
201
+ pub host: Option<String>,
202
+ /// Local ControlKit JSON-RPC port. Defaults to 12004.
203
+ #[serde(default)]
204
+ pub controlkit_port: Option<u16>,
205
+ /// Horizontal screen coordinate.
206
+ pub x: f32,
207
+ /// Vertical screen coordinate.
208
+ pub y: f32,
209
+}
210
+
211
#[derive(Debug, Default)]
212
pub struct XcrsMcpServer;
213
@@ -200,7 +236,11 @@ macro_rules! xcrs_mcp_tools {
236
$simulator_home_name:literal,
237
$simulator_button_name:literal,
238
$simulator_orientation_get_name:literal,
203
- $simulator_orientation_set_name:literal
239
+ $simulator_orientation_set_name:literal,
240
+ $controlkit_capabilities_name:literal,
241
+ $macos_click_name:literal,
242
+ $visionos_spatial_tap_name:literal,
243
+ $watchos_tap_name:literal
244
) => {
245
#[::rmcp::tool_router(router = xcrs_tool_router, vis = "pub(crate)")]
246
impl $server {
@@ -248,9 +288,193 @@ macro_rules! xcrs_mcp_tools {
288
))
289
}
290
291
+ fn controlkit_from_endpoint(
292
+ endpoint: &$crate::mcp::ControlKitEndpointArgs,
293
+ ) -> ::std::result::Result<
294
+ ($crate::ControlKit, Option<$crate::Simulator>),
295
+ ::rmcp::model::ErrorData,
296
+ > {
297
+ let simulator = match (&endpoint.simulator_udid, &endpoint.simulator_name) {
298
+ (Some(udid), _) => Some(
299
+ $crate::XcodeCommandLineTools::new()
300
+ .simctl()
301
+ .find_simulator_by_udid(udid)
302
+ .map_err(|error| {
303
+ ::rmcp::model::ErrorData::invalid_request(
304
+ error.to_string(),
305
+ None,
306
+ )
307
+ })?,
308
+ ),
309
+ (None, Some(name)) => Some(
310
+ $crate::XcodeCommandLineTools::new()
311
+ .simctl()
312
+ .find_simulator_by_name(name)
313
+ .map_err(|error| {
314
+ ::rmcp::model::ErrorData::invalid_request(
315
+ error.to_string(),
316
+ None,
317
+ )
318
+ })?,
319
+ ),
320
+ (None, None) => None,
321
+ };
322
+ let host = endpoint.host.as_deref().unwrap_or("127.0.0.1");
323
+ Ok((
324
+ $crate::ControlKit::with_host(
325
+ host,
326
+ endpoint.controlkit_port.unwrap_or(12004),
327
+ ),
328
+ simulator,
329
+ ))
330
+ }
331
+
332
+ fn endpoint_from_coordinate(
333
+ args: &$crate::mcp::ControlKitCoordinateArgs,
334
+ ) -> $crate::mcp::ControlKitEndpointArgs {
335
+ $crate::mcp::ControlKitEndpointArgs {
336
+ simulator_name: args.simulator_name.clone(),
337
+ simulator_udid: args.simulator_udid.clone(),
338
+ host: args.host.clone(),
339
+ controlkit_port: args.controlkit_port,
340
+ }
341
+ }
342
+
343
+ #[::rmcp::tool(
344
+ name = $controlkit_capabilities_name,
345
+ description = "Read the platform and supported capabilities from a ControlKit runner. Use a simulator name/UDID for iOS, tvOS, watchOS, or visionOS; omit them for a local macOS runner."
346
+ )]
347
+ async fn controlkit_capabilities(
348
+ &self,
349
+ ::rmcp::handler::server::wrapper::Parameters(
350
+ args,
351
+ ): ::rmcp::handler::server::wrapper::Parameters<
352
+ $crate::mcp::ControlKitEndpointArgs,
353
+ >,
354
+ ) -> ::std::result::Result<
355
+ ::rmcp::model::CallToolResult,
356
+ ::rmcp::model::ErrorData,
357
+ > {
358
+ let (controlkit, simulator) = Self::controlkit_from_endpoint(&args)?;
359
+ let result = controlkit
360
+ .call("device.capabilities", ::serde_json::json!({}))
361
+ .await
362
+ .map_err(|error| {
363
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
364
+ })?;
365
+ Ok(::rmcp::model::CallToolResult::success(vec![
366
+ ::rmcp::model::ContentBlock::json(&::serde_json::json!({
367
+ "simulator": simulator,
368
+ "capabilities": result,
369
+ }))?,
370
+ ]))
371
+ }
372
+
373
+ #[::rmcp::tool(
374
+ name = $macos_click_name,
375
+ description = "Click a macOS ControlKit runner at screen coordinates. Omit simulator_name and simulator_udid for the local Mac."
376
+ )]
377
+ async fn macos_click(
378
+ &self,
379
+ ::rmcp::handler::server::wrapper::Parameters(
380
+ args,
381
+ ): ::rmcp::handler::server::wrapper::Parameters<
382
+ $crate::mcp::ControlKitCoordinateArgs,
383
+ >,
384
+ ) -> ::std::result::Result<
385
+ ::rmcp::model::CallToolResult,
386
+ ::rmcp::model::ErrorData,
387
+ > {
388
+ let endpoint = Self::endpoint_from_coordinate(&args);
389
+ let (controlkit, _) = Self::controlkit_from_endpoint(&endpoint)?;
390
+ controlkit
391
+ .call(
392
+ "device.io.click",
393
+ ::serde_json::json!({ "x": args.x, "y": args.y }),
394
+ )
395
+ .await
396
+ .map_err(|error| {
397
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
398
+ })?;
399
+ Ok(::rmcp::model::CallToolResult::success(vec![
400
+ ::rmcp::model::ContentBlock::text(format!(
401
+ "Clicked ({}, {}).",
402
+ args.x, args.y
403
+ )),
404
+ ]))
405
+ }
406
+
407
+ #[::rmcp::tool(
408
+ name = $visionos_spatial_tap_name,
409
+ description = "Perform a spatial tap on a visionOS ControlKit runner."
410
+ )]
411
+ async fn visionos_spatial_tap(
412
+ &self,
413
+ ::rmcp::handler::server::wrapper::Parameters(
414
+ args,
415
+ ): ::rmcp::handler::server::wrapper::Parameters<
416
+ $crate::mcp::ControlKitCoordinateArgs,
417
+ >,
418
+ ) -> ::std::result::Result<
419
+ ::rmcp::model::CallToolResult,
420
+ ::rmcp::model::ErrorData,
421
+ > {
422
+ let endpoint = Self::endpoint_from_coordinate(&args);
423
+ let (controlkit, _) = Self::controlkit_from_endpoint(&endpoint)?;
424
+ controlkit
425
+ .call(
426
+ "device.io.spatial.tap",
427
+ ::serde_json::json!({ "x": args.x, "y": args.y }),
428
+ )
429
+ .await
430
+ .map_err(|error| {
431
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
432
+ })?;
433
+ Ok(::rmcp::model::CallToolResult::success(vec![
434
+ ::rmcp::model::ContentBlock::text(format!(
435
+ "Performed spatial tap at ({}, {}).",
436
+ args.x, args.y
437
+ )),
438
+ ]))
439
+ }
440
+
441
+ #[::rmcp::tool(
442
+ name = $watchos_tap_name,
443
+ description = "Tap a watchOS ControlKit runner at screen coordinates."
444
+ )]
445
+ async fn watchos_tap(
446
+ &self,
447
+ ::rmcp::handler::server::wrapper::Parameters(
448
+ args,
449
+ ): ::rmcp::handler::server::wrapper::Parameters<
450
+ $crate::mcp::ControlKitCoordinateArgs,
451
+ >,
452
+ ) -> ::std::result::Result<
453
+ ::rmcp::model::CallToolResult,
454
+ ::rmcp::model::ErrorData,
455
+ > {
456
+ let endpoint = Self::endpoint_from_coordinate(&args);
457
+ let (controlkit, _) = Self::controlkit_from_endpoint(&endpoint)?;
458
+ controlkit
459
+ .call(
460
+ "device.io.tap",
461
+ ::serde_json::json!({ "x": args.x, "y": args.y }),
462
+ )
463
+ .await
464
+ .map_err(|error| {
465
+ ::rmcp::model::ErrorData::internal_error(error.to_string(), None)
466
+ })?;
467
+ Ok(::rmcp::model::CallToolResult::success(vec![
468
+ ::rmcp::model::ContentBlock::text(format!(
469
+ "Tapped ({}, {}).",
470
+ args.x, args.y
471
+ )),
472
+ ]))
473
+ }
474
+
475
#[::rmcp::tool(
476
name = $simulator_list_name,
253
- description = "List all iOS simulators known to Xcode, including their runtime, UDID, availability, and current state."
477
+ description = "List all Apple simulator devices known to Xcode, including their platform, runtime, UDID, availability, and current state."
478
)]
479
async fn simulator_list(
480
&self,
@@ -271,7 +495,7 @@ macro_rules! xcrs_mcp_tools {
495
496
#[::rmcp::tool(
497
name = $simulator_find_name,
274
- description = "Find an iOS simulator by its exact name and return its details as JSON."
498
+ description = "Find an Apple simulator by its exact name and return its details as JSON."
499
)]
500
async fn simulator_find(
501
&self,
@@ -335,7 +559,7 @@ macro_rules! xcrs_mcp_tools {
559
560
#[::rmcp::tool(
561
name = $simulator_screenshot_name,
338
- description = "Capture a PNG screenshot from an iOS simulator."
562
+ description = "Capture a PNG screenshot from an Apple simulator."
563
)]
564
async fn simulator_screenshot(
565
&self,
@@ -363,7 +587,7 @@ macro_rules! xcrs_mcp_tools {
587
588
#[::rmcp::tool(
589
name = $simulator_launch_app_name,
366
- description = "Launch an installed app on an iOS simulator by bundle identifier."
590
+ description = "Launch an installed app on an Apple simulator by bundle identifier."
591
)]
592
async fn simulator_launch_app(
593
&self,
@@ -398,7 +622,7 @@ macro_rules! xcrs_mcp_tools {
622
623
#[::rmcp::tool(
624
name = $simulator_terminate_app_name,
401
- description = "Terminate an installed app on an iOS simulator by bundle identifier."
625
+ description = "Terminate an installed app on an Apple simulator by bundle identifier."
626
)]
627
async fn simulator_terminate_app(
628
&self,
@@ -433,7 +657,7 @@ macro_rules! xcrs_mcp_tools {
657
658
#[::rmcp::tool(
659
name = $simulator_open_url_name,
436
- description = "Open a URL or custom URL scheme on an iOS simulator."
660
+ description = "Open a URL or custom URL scheme on an Apple simulator."
661
)]
662
async fn simulator_open_url(
663
&self,
@@ -468,7 +692,7 @@ macro_rules! xcrs_mcp_tools {
692
693
#[::rmcp::tool(
694
name = $simulator_ui_dump_name,
471
- description = "Return the accessibility UI hierarchy from the foreground iOS app through ControlKit."
695
+ description = "Return the accessibility UI hierarchy from the foreground Apple app through ControlKit."
696
)]
697
async fn simulator_ui_dump(
698
&self,
@@ -502,7 +726,7 @@ macro_rules! xcrs_mcp_tools {
726
727
#[::rmcp::tool(
728
name = $simulator_list_elements_name,
505
- description = "List actionable accessibility elements and coordinates from the foreground iOS app through ControlKit."
729
+ description = "List actionable accessibility elements and coordinates from the foreground Apple app through ControlKit."
730
)]
731
async fn simulator_list_elements(
732
&self,
@@ -537,7 +761,7 @@ macro_rules! xcrs_mcp_tools {
761
762
#[::rmcp::tool(
763
name = $simulator_tap_name,
540
- description = "Tap the iOS simulator screen at the given coordinates through ControlKit."
764
+ description = "Tap an Apple simulator screen at the given coordinates through ControlKit."
765
)]
766
async fn simulator_tap(
767
&self,
@@ -574,7 +798,7 @@ macro_rules! xcrs_mcp_tools {
798
799
#[::rmcp::tool(
800
name = $simulator_text_name,
577
- description = "Type text into the focused iOS simulator field through ControlKit."
801
+ description = "Type text into the focused Apple simulator field through ControlKit."
802
)]
803
async fn simulator_text(
804
&self,
@@ -608,7 +832,7 @@ macro_rules! xcrs_mcp_tools {
832
833
#[::rmcp::tool(
834
name = $simulator_swipe_name,
611
- description = "Swipe between two screen coordinates on an iOS simulator through ControlKit."
835
+ description = "Swipe between two screen coordinates on an Apple simulator through ControlKit."
836
)]
837
async fn simulator_swipe(
838
&self,
@@ -650,7 +874,7 @@ macro_rules! xcrs_mcp_tools {
874
875
#[::rmcp::tool(
876
name = $simulator_home_name,
653
- description = "Press the iOS simulator Home button through ControlKit."
877
+ description = "Press the iOS Home button through ControlKit."
878
)]
879
async fn simulator_home(
880
&self,
@@ -833,7 +1057,11 @@ xcrs_mcp_tools!(
1057
"xcrs_simulator_press_home",
1058
"xcrs_simulator_press_button",
1059
"xcrs_simulator_orientation_get",
836
- "xcrs_simulator_orientation_set"
1060
+ "xcrs_simulator_orientation_set",
1061
+ "xcrs_controlkit_capabilities",
1062
+ "xcrs_macos_click",
1063
+ "xcrs_visionos_spatial_tap",
1064
+ "xcrs_watchos_tap"
1065
);
1066
1067
#[rmcp::tool_handler(router = Self::xcrs_tool_router())]
@@ -846,7 +1074,7 @@ impl ServerHandler for XcrsMcpServer {
1074
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
1075
.with_server_info(implementation)
1076
.with_instructions(
849
- "xcrs exposes Xcode command line tools for iOS simulators and app testing.",
1077
+ "xcrs exposes Xcode command line tools and ControlKit runners for Apple platform testing.",
1078
)
1079
}
1080
}
crates/xcrs/src/xcrs.rs
+49
-1
@@ -217,6 +217,8 @@ pub enum ApplePlatform {
217
Ios,
218
#[serde(rename = "tvOS")]
219
Tvos,
220
+ #[serde(rename = "macOS")]
221
+ Macos,
222
#[serde(rename = "watchOS")]
223
Watchos,
224
#[serde(rename = "visionOS")]
@@ -231,9 +233,12 @@ impl ApplePlatform {
233
Self::Ios
234
} else if runtime_identifier.contains(".tvOS-") {
235
Self::Tvos
236
+ } else if runtime_identifier.contains(".macOS-") {
237
+ Self::Macos
238
} else if runtime_identifier.contains(".watchOS-") {
239
Self::Watchos
236
- } else if runtime_identifier.contains(".visionOS-") {
240
+ } else if runtime_identifier.contains(".visionOS-") || runtime_identifier.contains(".xrOS-")
241
+ {
242
Self::Visionos
243
} else {
244
Self::Unknown
@@ -766,6 +771,49 @@ mod tests {
771
assert!(devices[0].is_booted());
772
}
773
774
+ #[test]
775
+ fn parses_xros_simulator_runtime() {
776
+ let output = r#"{
777
+ "devices": {
778
+ "com.apple.CoreSimulator.SimRuntime.xrOS-26-5": [
779
+ {
780
+ "udid": "00000000-0000-0000-0000-000000000001",
781
+ "isAvailable": true,
782
+ "state": "Shutdown",
783
+ "name": "Apple Vision Pro"
784
+ }
785
+ ]
786
+ }
787
+ }"#;
788
+
789
+ let devices = parse_simctl_devices(output).expect("devices should parse");
790
+
791
+ assert_eq!(devices.len(), 1);
792
+ assert_eq!(devices[0].platform, ApplePlatform::Visionos);
793
+ assert_eq!(devices[0].name, "Apple Vision Pro");
794
+ }
795
+
796
+ #[test]
797
+ fn parses_macos_runtime() {
798
+ let output = r#"{
799
+ "devices": {
800
+ "com.apple.CoreSimulator.SimRuntime.macOS-26-0": [
801
+ {
802
+ "udid": "00000000-0000-0000-0000-000000000002",
803
+ "isAvailable": true,
804
+ "state": "Booted",
805
+ "name": "Mac"
806
+ }
807
+ ]
808
+ }
809
+ }"#;
810
+
811
+ let devices = parse_simctl_devices(output).expect("devices should parse");
812
+
813
+ assert_eq!(devices.len(), 1);
814
+ assert_eq!(devices[0].platform, ApplePlatform::Macos);
815
+ }
816
+
817
#[test]
818
fn formats_ipv6_controlkit_url() {
819
let controlkit = ControlKit::with_host("fdb4:e020:7377::1", 12006);
docs/mcp.md
+35
-2
@@ -4,8 +4,9 @@ The smbCloud CLI can run as a **Model Context Protocol (MCP) server**, so an AI
4
assistant or agent — Claude Desktop, Claude Code, Cursor, or any other
5
MCP-capable client — can manage your smbCloud projects, tenants, Mail apps, and
6
Auth apps directly, without you leaving the chat to run `smb` commands by hand.
7
-Start it with `smb --mcp`; it speaks standard MCP over stdio and exposes 30
8
-tools covering the account, project, tenant, Mail, and Auth surfaces.
7
+Start it with `smb --mcp`; it speaks standard MCP over stdio and exposes 50
8
+tools covering the account, project, tenant, Mail, Auth, simulator, and
9
+ControlKit runner surfaces.
10
11
This page is the setup guide and the full tool reference. For how `--mcp`
12
compares to the CLI's other two interfaces (headless and `--tui`), see
@@ -212,6 +213,38 @@ users; it belongs to a project.
213
| `auth_app_update` | `id`, `name`/`support_email` (at least one) | The updated Auth app. |
214
| `auth_app_delete` | `id` | Confirmation. **Destructive and irreversible.** |
215
216
+### ControlKit runners
217
+
218
+The `smb` MCP server also exposes local Xcode simulator and ControlKit runner
219
+tools. Simulator arguments accept either `simulator_name` or `simulator_udid`;
220
+ControlKit arguments default to `127.0.0.1:12004`. For a local macOS runner,
221
+omit the simulator fields and use the optional `host` and `controlkit_port`
222
+fields.
223
+
224
+| Tool | Arguments | Returns |
225
+| --- | --- | --- |
226
+| `smb_simulator_list` | _(none)_ | All Apple simulator devices known to Xcode. |
227
+| `smb_simulator_find` | `name` | A simulator's details. |
228
+| `smb_controlkit_capabilities` | simulator target or optional `host`, `controlkit_port` | Runner platform and capabilities. |
229
+| `smb_macos_click` | `x`, `y`, plus optional endpoint fields | A pointer click on a macOS runner. |
230
+| `smb_visionos_spatial_tap` | `x`, `y`, plus optional endpoint fields | A spatial tap on a visionOS runner. |
231
+| `smb_watchos_tap` | `x`, `y`, plus optional endpoint fields | A touch tap on a watchOS runner. |
232
+| `smb_simulator_tap` | simulator target, `x`, `y` | A touch tap through ControlKit. |
233
+| `smb_simulator_type_text` | simulator target, `text` | Text input through ControlKit. |
234
+| `smb_simulator_swipe` | simulator target, `x1`, `y1`, `x2`, `y2` | A swipe through ControlKit. |
235
+| `smb_simulator_press_button` | simulator target, `button` | A supported remote-button press. |
236
+| `smb_simulator_press_home` | simulator target | An iOS Home-button press. |
237
+| `smb_simulator_orientation_get`/`set` | simulator target; `orientation` for `set` | Current or updated iOS orientation. |
238
+| `smb_simulator_launch_app`/`terminate_app` | simulator target, `bundle_id` | App lifecycle confirmation. |
239
+| `smb_simulator_open_url` | simulator target, `url` | URL-open confirmation. |
240
+| `smb_simulator_ui_dump` | simulator target | Accessibility UI hierarchy. |
241
+| `smb_simulator_list_elements` | simulator target | Actionable accessibility elements. |
242
+| `smb_simulator_screenshot` | simulator target | A PNG image. |
243
+| `smb_ios_app_test` | simulator target, `app_path`, `bundle_id` | Installed/launched app details. |
244
+
245
+The standalone `xcrs --mcp` server exposes the same runner surface with the
246
+`xcrs_` prefix instead of `smb_`.
247
+
248
## Safety: tools run without confirmation
249
250
The CLI's own `project delete`, `tenant delete`, and similar commands ask for