From 4dce6e155f207a2f18c0b1abb6925261dee2fb7a Mon Sep 17 00:00:00 2001 From: Seto Elkahfi <1797197+setoelkahfi@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:35:12 +0200 Subject: [PATCH] Fix MCP not loaded on GitHub Copilot CLI --- Cargo.lock | 77 +++++++++++++------------------------ Cargo.toml | 2 +- crates/cli/src/mcp.rs | 12 +++--- crates/cli/src/mcp_xcrs.rs | 9 ++--- crates/xcrs/src/mcp.rs | 79 +++++++++++++++++++++++++++++++++++--- 5 files changed, 109 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 524417e..109180c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -144,6 +144,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bitflags" version = "1.3.2" @@ -480,37 +486,14 @@ dependencies = [ "cmov", ] -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - [[package]] name = "darling" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ - "darling_core 0.24.1", - "darling_macro 0.24.1", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim 0.11.1", - "syn 2.0.119", + "darling_core", + "darling_macro", ] [[package]] @@ -526,24 +509,13 @@ dependencies = [ "syn 3.0.4", ] -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.119", -] - [[package]] name = "darling_macro" version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ - "darling_core 0.24.1", + "darling_core", "quote", "syn 3.0.4", ] @@ -1040,7 +1012,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -1199,6 +1171,8 @@ checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", "hashbrown 0.17.1", + "serde", + "serde_core", ] [[package]] @@ -1229,7 +1203,7 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8" dependencies = [ - "darling 0.24.1", + "darling", "indoc", "proc-macro2", "quote", @@ -1749,7 +1723,7 @@ version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ - "base64", + "base64 0.22.1", "byteorder", "bytes", "fallible-iterator", @@ -2092,7 +2066,7 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", "http", @@ -2141,14 +2115,14 @@ dependencies = [ [[package]] name = "rmcp" -version = "2.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14db48ee17a9ba61810ab1a9c1beb7d06d8136ae39ac25a1137f10d357af01af" +checksum = "5b6317cd8c13e3ec9033cf2aa5aa92cd743f0f4f8a93cddc42ea0ae6ce3b8898" dependencies = [ - "async-trait", - "base64", + "base64 0.23.1", "chrono", "futures", + "indexmap", "pastey", "pin-project-lite", "rmcp-macros", @@ -2159,19 +2133,20 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "uuid", ] [[package]] name = "rmcp-macros" -version = "2.2.0" +version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783d787bf21813b285f13019adc49e11af501c658890c1e519f31f937c68b7e3" +checksum = "d2ee366004e5f87c103be91c6c09337811517ae8d2f919ece9da7c21444b85c5" dependencies = [ - "darling 0.23.0", + "darling", "proc-macro2", "quote", "serde_json", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -2558,7 +2533,7 @@ dependencies = [ name = "smbcloud-auth-sdk" version = "0.5.4" dependencies = [ - "base64", + "base64 0.22.1", "reqwest", "serde", "serde_json", @@ -4000,7 +3975,7 @@ name = "xcrs" version = "0.5.4" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "clap 4.6.6", "reqwest", "rmcp", diff --git a/Cargo.toml b/Cargo.toml index 520ef27..6070875 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ openssl = { version = "0.10", features = ["vendored"] } ratatui = { version = "0.29", default-features = false, features = ["crossterm"] } regex = "1.11" reqwest = { version = "0.12.18", default-features = false } -rmcp = { version = "2.2.0", features = ["server", "macros", "transport-io", "schemars"] } +rmcp = { version = "3.4.1", features = ["server", "macros", "transport-io", "schemars"] } schemars = "1" serde = "1" serde_json = "1.0.82" diff --git a/crates/cli/src/mcp.rs b/crates/cli/src/mcp.rs index 4cf5e23..fb7b4d5 100644 --- a/crates/cli/src/mcp.rs +++ b/crates/cli/src/mcp.rs @@ -29,10 +29,8 @@ use { anyhow::{anyhow, Result}, rmcp::{ handler::server::{wrapper::Parameters, ServerHandler}, - model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo}, - tool, tool_handler, tool_router, - transport::stdio, - ErrorData, ServiceExt, + model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerConfig}, + tool, tool_handler, tool_router, ErrorData, ServiceExt, }, schemars::JsonSchema, serde::Deserialize, @@ -982,14 +980,14 @@ impl SmbMcpServer { #[tool_handler(router = Self::cloud_tool_router())] impl ServerHandler for SmbMcpServer { - fn get_info(&self) -> ServerInfo { + fn get_info(&self) -> ServerConfig { // `Implementation` is `#[non_exhaustive]`, so start from the build-env // default and override the identity fields to report `smb`, not `rmcp`. let mut server_info = Implementation::from_build_env(); server_info.name = "smb".to_string(); server_info.version = env!("CARGO_PKG_VERSION").to_string(); - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(server_info) .with_instructions( "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<()> { } let running = SmbMcpServer::new(environment) - .serve(stdio()) + .serve(xcrs::mcp::compatible_stdio().await?) .await .map_err(|e| anyhow!("Failed to start MCP server: {e}"))?; running diff --git a/crates/cli/src/mcp_xcrs.rs b/crates/cli/src/mcp_xcrs.rs index cea3f94..7fa923d 100644 --- a/crates/cli/src/mcp_xcrs.rs +++ b/crates/cli/src/mcp_xcrs.rs @@ -9,8 +9,7 @@ use { anyhow::{anyhow, Result}, rmcp::{ - model::{Implementation, ServerCapabilities, ServerInfo}, - transport::stdio, + model::{Implementation, ServerCapabilities, ServerConfig}, ServerHandler, ServiceExt, }, }; @@ -52,14 +51,14 @@ xcrs::xcrs_mcp_tools!( #[rmcp::tool_handler(router = Self::xcrs_tool_router())] impl ServerHandler for AutomationMcpServer { - fn get_info(&self) -> ServerInfo { + fn get_info(&self) -> ServerConfig { // `Implementation` is `#[non_exhaustive]`, so start from the build-env // default and override the identity fields. let mut server_info = Implementation::from_build_env(); server_info.name = "XCRS Mobile & TV Automation".to_string(); server_info.version = env!("CARGO_PKG_VERSION").to_string(); - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(server_info) .with_instructions( "Cross-platform mobile and TV app automation over Apple simulators/ControlKit \ @@ -76,7 +75,7 @@ impl ServerHandler for AutomationMcpServer { /// Run the automation MCP server over stdio until the client disconnects. pub async fn serve() -> Result<()> { let running = AutomationMcpServer::new() - .serve(stdio()) + .serve(xcrs::mcp::compatible_stdio().await?) .await .map_err(|error| anyhow!("Failed to start automation MCP server: {error}"))?; running diff --git a/crates/xcrs/src/mcp.rs b/crates/xcrs/src/mcp.rs index 13636cd..0ed9824 100644 --- a/crates/xcrs/src/mcp.rs +++ b/crates/xcrs/src/mcp.rs @@ -17,15 +17,60 @@ use { anyhow::{anyhow, Result}, rmcp::{ - model::{Implementation, ServerCapabilities, ServerInfo}, - transport::stdio, + model::{Implementation, ServerCapabilities, ServerConfig}, ServerHandler, ServiceExt, }, schemars::JsonSchema, serde::{Deserialize, Serialize}, - std::path::PathBuf, + serde_json::{json, Value}, + std::{io::Cursor, path::PathBuf}, + tokio::io::{ + stdin, stdout, AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWriteExt, BufReader, Stdout, + }, }; +type StdioReader = Box; + +/// Preserve compatibility with clients that send the legacy, metadata-free +/// `server/discover` probe before falling back to `initialize`. +pub async fn compatible_stdio() -> Result<(StdioReader, Stdout)> { + let mut reader = BufReader::new(stdin()); + let mut first_line = String::new(); + reader.read_line(&mut first_line).await?; + + let mut output = stdout(); + if let Some(response) = legacy_discovery_response(&first_line) { + output.write_all(&response).await?; + output.flush().await?; + return Ok((Box::new(reader), output)); + } + + let input = Cursor::new(first_line.into_bytes()).chain(reader); + Ok((Box::new(input), output)) +} + +fn legacy_discovery_response(line: &str) -> Option> { + let request: Value = serde_json::from_str(line).ok()?; + if request.get("method")?.as_str()? != "server/discover" + || request.get("params")?.get("_meta").is_some() + { + return None; + } + + let id = request.get("id")?.clone(); + let mut response = serde_json::to_vec(&json!({ + "jsonrpc": "2.0", + "id": id, + "error": { + "code": -32601, + "message": "Method 'server/discover' is not available without request metadata." + } + })) + .ok()?; + response.push(b'\n'); + Some(response) +} + /// A validated, unambiguous automation target: exactly one of an Apple simulator, an /// Apple ControlKit host (physical device, remote runner, or local Mac), or an /// Android device. Produced by [`resolve_selected_target`], never constructed @@ -2008,12 +2053,12 @@ xcrs_mcp_tools!( #[rmcp::tool_handler(router = Self::xcrs_tool_router())] impl ServerHandler for XcrsMcpServer { - fn get_info(&self) -> ServerInfo { + fn get_info(&self) -> ServerConfig { let mut implementation = Implementation::from_build_env(); implementation.name = "xcrs".to_string(); implementation.version = env!("CARGO_PKG_VERSION").to_string(); - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + ServerConfig::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(implementation) .with_instructions( "xcrs exposes a canonical cross-platform automation contract (device_list, \ @@ -2028,7 +2073,7 @@ impl ServerHandler for XcrsMcpServer { pub async fn serve() -> Result<()> { let running = XcrsMcpServer::new() - .serve(stdio()) + .serve(compatible_stdio().await?) .await .map_err(|error| anyhow!("Failed to start xcrs MCP server: {error}"))?; running @@ -2042,6 +2087,28 @@ pub async fn serve() -> Result<()> { mod tests { use super::*; + #[test] + fn legacy_discovery_probe_receives_method_not_found() { + let response = legacy_discovery_response( + r#"{"jsonrpc":"2.0","id":0,"method":"server/discover","params":{}}"#, + ) + .expect("legacy discovery probe should receive a fallback response"); + let response: Value = + serde_json::from_slice(&response).expect("response should be valid JSON"); + + assert_eq!(response["id"], 0); + assert_eq!(response["error"]["code"], -32601); + } + + #[test] + fn modern_discovery_probe_passes_through() { + let response = legacy_discovery_response( + r#"{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}"#, + ); + + assert!(response.is_none()); + } + /// The 19 canonical tool names shared by the standalone and embedded /// automation servers. const CANONICAL_TOOL_NAMES: [&str; 19] = [