5 files changed
+109
-70
Cargo.lock
+26
-51
@@ -144,6 +144,12 @@ version = "0.22.1"
144
source = "registry+https://github.com/rust-lang/crates.io-index"
145
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
146
147
+[[package]]
148
+name = "base64"
149
+version = "0.23.1"
150
+source = "registry+https://github.com/rust-lang/crates.io-index"
151
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
152
+
153
[[package]]
154
name = "bitflags"
155
version = "1.3.2"
@@ -480,37 +486,14 @@ dependencies = [
486
"cmov",
487
]
488
483
-[[package]]
484
-name = "darling"
485
-version = "0.23.0"
486
-source = "registry+https://github.com/rust-lang/crates.io-index"
487
-checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
488
-dependencies = [
489
- "darling_core 0.23.0",
490
- "darling_macro 0.23.0",
491
-]
492
-
489
[[package]]
490
name = "darling"
491
version = "0.24.1"
492
source = "registry+https://github.com/rust-lang/crates.io-index"
493
checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec"
494
dependencies = [
499
- "darling_core 0.24.1",
500
- "darling_macro 0.24.1",
501
-]
502
-
503
-[[package]]
504
-name = "darling_core"
505
-version = "0.23.0"
506
-source = "registry+https://github.com/rust-lang/crates.io-index"
507
-checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
508
-dependencies = [
509
- "ident_case",
510
- "proc-macro2",
511
- "quote",
512
- "strsim 0.11.1",
513
- "syn 2.0.119",
495
+ "darling_core",
496
+ "darling_macro",
497
]
498
499
[[package]]
@@ -526,24 +509,13 @@ dependencies = [
509
"syn 3.0.4",
510
]
511
529
-[[package]]
530
-name = "darling_macro"
531
-version = "0.23.0"
532
-source = "registry+https://github.com/rust-lang/crates.io-index"
533
-checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
534
-dependencies = [
535
- "darling_core 0.23.0",
536
- "quote",
537
- "syn 2.0.119",
538
-]
539
-
512
[[package]]
513
name = "darling_macro"
514
version = "0.24.1"
515
source = "registry+https://github.com/rust-lang/crates.io-index"
516
checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785"
517
dependencies = [
546
- "darling_core 0.24.1",
518
+ "darling_core",
519
"quote",
520
"syn 3.0.4",
521
]
@@ -1040,7 +1012,7 @@ version = "0.1.20"
1012
source = "registry+https://github.com/rust-lang/crates.io-index"
1013
checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
1014
dependencies = [
1043
- "base64",
1015
+ "base64 0.22.1",
1016
"bytes",
1017
"futures-channel",
1018
"futures-util",
@@ -1199,6 +1171,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
1171
dependencies = [
1172
"equivalent",
1173
"hashbrown 0.17.1",
1174
+ "serde",
1175
+ "serde_core",
1176
]
1177
1178
[[package]]
@@ -1229,7 +1203,7 @@ version = "0.3.13"
1203
source = "registry+https://github.com/rust-lang/crates.io-index"
1204
checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8"
1205
dependencies = [
1232
- "darling 0.24.1",
1206
+ "darling",
1207
"indoc",
1208
"proc-macro2",
1209
"quote",
@@ -1749,7 +1723,7 @@ version = "0.6.12"
1723
source = "registry+https://github.com/rust-lang/crates.io-index"
1724
checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514"
1725
dependencies = [
1752
- "base64",
1726
+ "base64 0.22.1",
1727
"byteorder",
1728
"bytes",
1729
"fallible-iterator",
@@ -2092,7 +2066,7 @@ version = "0.12.28"
2066
source = "registry+https://github.com/rust-lang/crates.io-index"
2067
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
2068
dependencies = [
2095
- "base64",
2069
+ "base64 0.22.1",
2070
"bytes",
2071
"futures-core",
2072
"http",
@@ -2141,14 +2115,14 @@ dependencies = [
2115
2116
[[package]]
2117
name = "rmcp"
2144
-version = "2.2.0"
2118
+version = "3.4.1"
2119
source = "registry+https://github.com/rust-lang/crates.io-index"
2146
-checksum = "14db48ee17a9ba61810ab1a9c1beb7d06d8136ae39ac25a1137f10d357af01af"
2120
+checksum = "5b6317cd8c13e3ec9033cf2aa5aa92cd743f0f4f8a93cddc42ea0ae6ce3b8898"
2121
dependencies = [
2148
- "async-trait",
2149
- "base64",
2122
+ "base64 0.23.1",
2123
"chrono",
2124
"futures",
2125
+ "indexmap",
2126
"pastey",
2127
"pin-project-lite",
2128
"rmcp-macros",
@@ -2159,19 +2133,20 @@ dependencies = [
2133
"tokio",
2134
"tokio-util",
2135
"tracing",
2136
+ "uuid",
2137
]
2138
2139
[[package]]
2140
name = "rmcp-macros"
2166
-version = "2.2.0"
2141
+version = "3.4.1"
2142
source = "registry+https://github.com/rust-lang/crates.io-index"
2168
-checksum = "783d787bf21813b285f13019adc49e11af501c658890c1e519f31f937c68b7e3"
2143
+checksum = "d2ee366004e5f87c103be91c6c09337811517ae8d2f919ece9da7c21444b85c5"
2144
dependencies = [
2170
- "darling 0.23.0",
2145
+ "darling",
2146
"proc-macro2",
2147
"quote",
2148
"serde_json",
2174
- "syn 2.0.119",
2149
+ "syn 3.0.4",
2150
]
2151
2152
[[package]]
@@ -2558,7 +2533,7 @@ dependencies = [
2533
name = "smbcloud-auth-sdk"
2534
version = "0.5.4"
2535
dependencies = [
2561
- "base64",
2536
+ "base64 0.22.1",
2537
"reqwest",
2538
"serde",
2539
"serde_json",
@@ -4000,7 +3975,7 @@ name = "xcrs"
3975
version = "0.5.4"
3976
dependencies = [
3977
"anyhow",
4003
- "base64",
3978
+ "base64 0.22.1",
3979
"clap 4.6.6",
3980
"reqwest",
3981
"rmcp",
Cargo.toml
+1
-1
@@ -24,7 +24,7 @@ openssl = { version = "0.10", features = ["vendored"] }
24
ratatui = { version = "0.29", default-features = false, features = ["crossterm"] }
25
regex = "1.11"
26
reqwest = { version = "0.12.18", default-features = false }
27
-rmcp = { version = "2.2.0", features = ["server", "macros", "transport-io", "schemars"] }
27
+rmcp = { version = "3.4.1", features = ["server", "macros", "transport-io", "schemars"] }
28
schemars = "1"
29
serde = "1"
30
serde_json = "1.0.82"
crates/cli/src/mcp.rs
+5
-7
@@ -29,10 +29,8 @@ use {
29
anyhow::{anyhow, Result},
30
rmcp::{
31
handler::server::{wrapper::Parameters, ServerHandler},
32
- model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo},
33
- tool, tool_handler, tool_router,
34
- transport::stdio,
35
- ErrorData, ServiceExt,
32
+ model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerConfig},
33
+ tool, tool_handler, tool_router, ErrorData, ServiceExt,
34
},
35
schemars::JsonSchema,
36
serde::Deserialize,
@@ -982,14 +980,14 @@ impl SmbMcpServer {
980
981
#[tool_handler(router = Self::cloud_tool_router())]
982
impl ServerHandler for SmbMcpServer {
985
- fn get_info(&self) -> ServerInfo {
983
+ fn get_info(&self) -> ServerConfig {
984
// `Implementation` is `#[non_exhaustive]`, so start from the build-env
985
// default and override the identity fields to report `smb`, not `rmcp`.
986
let mut server_info = Implementation::from_build_env();
987
server_info.name = "smb".to_string();
988
server_info.version = env!("CARGO_PKG_VERSION").to_string();
989
992
- ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
990
+ ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
991
.with_server_info(server_info)
992
.with_instructions(
993
"smbCloud CLI exposed as MCP tools. Authentication uses the token stored by \
@@ -1008,7 +1006,7 @@ pub async fn serve(environment: Environment, scope: McpScope) -> Result<()> {
1006
}
1007
1008
let running = SmbMcpServer::new(environment)
1011
- .serve(stdio())
1009
+ .serve(xcrs::mcp::compatible_stdio().await?)
1010
.await
1011
.map_err(|e| anyhow!("Failed to start MCP server: {e}"))?;
1012
running
crates/cli/src/mcp_xcrs.rs
+4
-5
@@ -9,8 +9,7 @@
9
use {
10
anyhow::{anyhow, Result},
11
rmcp::{
12
- model::{Implementation, ServerCapabilities, ServerInfo},
13
- transport::stdio,
12
+ model::{Implementation, ServerCapabilities, ServerConfig},
13
ServerHandler, ServiceExt,
14
},
15
};
@@ -52,14 +51,14 @@ xcrs::xcrs_mcp_tools!(
51
52
#[rmcp::tool_handler(router = Self::xcrs_tool_router())]
53
impl ServerHandler for AutomationMcpServer {
55
- fn get_info(&self) -> ServerInfo {
54
+ fn get_info(&self) -> ServerConfig {
55
// `Implementation` is `#[non_exhaustive]`, so start from the build-env
56
// default and override the identity fields.
57
let mut server_info = Implementation::from_build_env();
58
server_info.name = "XCRS Mobile & TV Automation".to_string();
59
server_info.version = env!("CARGO_PKG_VERSION").to_string();
60
62
- ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
61
+ ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
62
.with_server_info(server_info)
63
.with_instructions(
64
"Cross-platform mobile and TV app automation over Apple simulators/ControlKit \
@@ -76,7 +75,7 @@ impl ServerHandler for AutomationMcpServer {
75
/// Run the automation MCP server over stdio until the client disconnects.
76
pub async fn serve() -> Result<()> {
77
let running = AutomationMcpServer::new()
79
- .serve(stdio())
78
+ .serve(xcrs::mcp::compatible_stdio().await?)
79
.await
80
.map_err(|error| anyhow!("Failed to start automation MCP server: {error}"))?;
81
running
crates/xcrs/src/mcp.rs
+73
-6
@@ -17,15 +17,60 @@
17
use {
18
anyhow::{anyhow, Result},
19
rmcp::{
20
- model::{Implementation, ServerCapabilities, ServerInfo},
21
- transport::stdio,
20
+ model::{Implementation, ServerCapabilities, ServerConfig},
21
ServerHandler, ServiceExt,
22
},
23
schemars::JsonSchema,
24
serde::{Deserialize, Serialize},
26
- std::path::PathBuf,
25
+ serde_json::{json, Value},
26
+ std::{io::Cursor, path::PathBuf},
27
+ tokio::io::{
28
+ stdin, stdout, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWriteExt, BufReader, Stdout,
29
+ },
30
};
31
32
+type StdioReader = Box<dyn AsyncRead + Send + Unpin>;
33
+
34
+/// Preserve compatibility with clients that send the legacy, metadata-free
35
+/// `server/discover` probe before falling back to `initialize`.
36
+pub async fn compatible_stdio() -> Result<(StdioReader, Stdout)> {
37
+ let mut reader = BufReader::new(stdin());
38
+ let mut first_line = String::new();
39
+ reader.read_line(&mut first_line).await?;
40
+
41
+ let mut output = stdout();
42
+ if let Some(response) = legacy_discovery_response(&first_line) {
43
+ output.write_all(&response).await?;
44
+ output.flush().await?;
45
+ return Ok((Box::new(reader), output));
46
+ }
47
+
48
+ let input = Cursor::new(first_line.into_bytes()).chain(reader);
49
+ Ok((Box::new(input), output))
50
+}
51
+
52
+fn legacy_discovery_response(line: &str) -> Option<Vec<u8>> {
53
+ let request: Value = serde_json::from_str(line).ok()?;
54
+ if request.get("method")?.as_str()? != "server/discover"
55
+ || request.get("params")?.get("_meta").is_some()
56
+ {
57
+ return None;
58
+ }
59
+
60
+ let id = request.get("id")?.clone();
61
+ let mut response = serde_json::to_vec(&json!({
62
+ "jsonrpc": "2.0",
63
+ "id": id,
64
+ "error": {
65
+ "code": -32601,
66
+ "message": "Method 'server/discover' is not available without request metadata."
67
+ }
68
+ }))
69
+ .ok()?;
70
+ response.push(b'\n');
71
+ Some(response)
72
+}
73
+
74
/// A validated, unambiguous automation target: exactly one of an Apple simulator, an
75
/// Apple ControlKit host (physical device, remote runner, or local Mac), or an
76
/// Android device. Produced by [`resolve_selected_target`], never constructed
@@ -2008,12 +2053,12 @@ xcrs_mcp_tools!(
2053
2054
#[rmcp::tool_handler(router = Self::xcrs_tool_router())]
2055
impl ServerHandler for XcrsMcpServer {
2011
- fn get_info(&self) -> ServerInfo {
2056
+ fn get_info(&self) -> ServerConfig {
2057
let mut implementation = Implementation::from_build_env();
2058
implementation.name = "xcrs".to_string();
2059
implementation.version = env!("CARGO_PKG_VERSION").to_string();
2060
2016
- ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
2061
+ ServerConfig::new(ServerCapabilities::builder().enable_tools().build())
2062
.with_server_info(implementation)
2063
.with_instructions(
2064
"xcrs exposes a canonical cross-platform automation contract (device_list, \
@@ -2028,7 +2073,7 @@ impl ServerHandler for XcrsMcpServer {
2073
2074
pub async fn serve() -> Result<()> {
2075
let running = XcrsMcpServer::new()
2031
- .serve(stdio())
2076
+ .serve(compatible_stdio().await?)
2077
.await
2078
.map_err(|error| anyhow!("Failed to start xcrs MCP server: {error}"))?;
2079
running
@@ -2042,6 +2087,28 @@ pub async fn serve() -> Result<()> {
2087
mod tests {
2088
use super::*;
2089
2090
+ #[test]
2091
+ fn legacy_discovery_probe_receives_method_not_found() {
2092
+ let response = legacy_discovery_response(
2093
+ r#"{"jsonrpc":"2.0","id":0,"method":"server/discover","params":{}}"#,
2094
+ )
2095
+ .expect("legacy discovery probe should receive a fallback response");
2096
+ let response: Value =
2097
+ serde_json::from_slice(&response).expect("response should be valid JSON");
2098
+
2099
+ assert_eq!(response["id"], 0);
2100
+ assert_eq!(response["error"]["code"], -32601);
2101
+ }
2102
+
2103
+ #[test]
2104
+ fn modern_discovery_probe_passes_through() {
2105
+ let response = legacy_discovery_response(
2106
+ r#"{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}"#,
2107
+ );
2108
+
2109
+ assert!(response.is_none());
2110
+ }
2111
+
2112
/// The 19 canonical tool names shared by the standalone and embedded
2113
/// automation servers.
2114
const CANONICAL_TOOL_NAMES: [&str; 19] = [