@smbcloud / cli / commits / 4dce6e1

Fix MCP not loaded on GitHub Copilot CLI

Seto Elkahfi committed Sep 27, 2026 at 21:35 UTC 4dce6e155f207a2f18c0b1abb6925261dee2fb7a
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] = [