Skip to content

Commit 27112e1

Browse files
its-mashclaude
andcommitted
feat: add Docker-specific hints for stdio connection errors
When a stdio MCP server uses docker as the command and fails to connect, the error message now includes "Ensure Docker Desktop is installed and running." This helps developers who have Docker stopped or not installed. Also fixes e2e desktop CI flakiness on Ubuntu by combining gnome-keyring-daemon --unlock and --start into a single invocation with eval to properly export GNOME_KEYRING_CONTROL. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 1d20ff3 commit 27112e1

3 files changed

Lines changed: 68 additions & 7 deletions

File tree

.github/workflows/e2e-desktop.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,10 +115,11 @@ jobs:
115115
- name: Run desktop E2E tests (Linux)
116116
if: matrix.os == 'ubuntu-latest'
117117
run: |
118-
# Start dbus session and unlock gnome-keyring with a dummy password for CI
118+
# Start dbus session and unlock gnome-keyring with a dummy password for CI.
119+
# Use a single daemon invocation (--unlock + --start) and eval to export
120+
# GNOME_KEYRING_CONTROL so the keyring stays unlocked across all test specs.
119121
dbus-run-session -- bash -c '
120-
echo "test" | gnome-keyring-daemon --unlock --components=secrets
121-
export $(gnome-keyring-daemon --start --components=secrets)
122+
eval $(echo "test" | gnome-keyring-daemon --unlock --start --components=secrets 2>/dev/null)
122123
sleep 1
123124
xvfb-run --auto-servernum pnpm test:e2e
124125
'

crates/mcpmux-gateway/src/pool/transport/stdio.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ pub fn configure_child_process_platform(cmd: &mut Command) {
4141
}
4242
}
4343

44+
/// Returns a helpful hint for common runtime-dependent commands when they fail.
45+
fn command_hint(command: &str) -> &'static str {
46+
let cmd = command.rsplit(['/', '\\']).next().unwrap_or(command);
47+
if cmd == "docker" || cmd == "docker.exe" || cmd.starts_with("docker-") {
48+
" Ensure Docker Desktop is installed and running."
49+
} else {
50+
""
51+
}
52+
}
53+
4454
/// STDIO transport for child process MCP servers
4555
pub struct StdioTransport {
4656
command: String,
@@ -114,8 +124,9 @@ impl Transport for StdioTransport {
114124
{
115125
Ok(path) => path,
116126
Err(_) => {
127+
let hint = command_hint(&self.command);
117128
let err = format!(
118-
"Command not found: {}. Ensure it's installed and in PATH.",
129+
"Command not found: {}. Ensure it's installed and in PATH.{hint}",
119130
self.command
120131
);
121132
error!(server_id = %self.server_id, "{}", err);
@@ -156,7 +167,8 @@ impl Transport for StdioTransport {
156167
})) {
157168
Ok(t) => t,
158169
Err(e) => {
159-
let err = format!("Failed to spawn process: {}", e);
170+
let hint = command_hint(&self.command);
171+
let err = format!("Failed to spawn process: {e}.{hint}");
160172
error!(server_id = %self.server_id, "{}", err);
161173
self.log(LogLevel::Error, LogSource::Connection, err.clone())
162174
.await;
@@ -173,14 +185,16 @@ impl Transport for StdioTransport {
173185
let client = match tokio::time::timeout(self.connect_timeout, connect_future).await {
174186
Ok(Ok(client)) => client,
175187
Ok(Err(e)) => {
176-
let err = format!("MCP handshake failed: {}", e);
188+
let hint = command_hint(&self.command);
189+
let err = format!("MCP handshake failed: {e}.{hint}");
177190
error!(server_id = %self.server_id, "{}", err);
178191
self.log(LogLevel::Error, LogSource::Connection, err.clone())
179192
.await;
180193
return TransportConnectResult::Failed(err);
181194
}
182195
Err(_) => {
183-
let err = format!("Connection timeout ({:?})", self.connect_timeout);
196+
let hint = command_hint(&self.command);
197+
let err = format!("Connection timeout ({:?}).{hint}", self.connect_timeout);
184198
error!(server_id = %self.server_id, "{}", err);
185199
self.log(LogLevel::Error, LogSource::Connection, err.clone())
186200
.await;

tests/rust/tests/gateway/stdio_transport.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,52 @@ async fn test_platform_flags_idempotent() {
170170
assert!(status.success(), "Child process should exit successfully");
171171
}
172172

173+
/// Verify that a docker command not found error includes a Docker-specific hint
174+
#[tokio::test]
175+
async fn test_docker_command_not_found_includes_hint() {
176+
use mcpmux_gateway::pool::transport::StdioTransport;
177+
use mcpmux_gateway::pool::{Transport, TransportConnectResult};
178+
use std::collections::HashMap;
179+
use std::time::Duration;
180+
use uuid::Uuid;
181+
182+
let transport = StdioTransport::new(
183+
"docker".to_string(),
184+
vec![
185+
"run".to_string(),
186+
"-i".to_string(),
187+
"some-image".to_string(),
188+
],
189+
HashMap::new(),
190+
Uuid::new_v4(),
191+
"test-docker-server".to_string(),
192+
None,
193+
Duration::from_secs(5),
194+
None,
195+
);
196+
197+
let result = transport.connect().await;
198+
match result {
199+
TransportConnectResult::Failed(msg) => {
200+
// If docker is not installed, we get "Command not found" with hint.
201+
// If docker IS installed but daemon isn't running, we'd get a different error with hint.
202+
// Either way, the hint should be present.
203+
assert!(
204+
msg.contains("Docker Desktop"),
205+
"Expected Docker hint in error message, got: {msg}"
206+
);
207+
}
208+
// If docker happens to be installed and running, the test still passes
209+
// (connect would succeed or fail with handshake error that includes the hint)
210+
TransportConnectResult::Connected(_) => {
211+
// Docker is installed and running - that's fine, test passes
212+
}
213+
TransportConnectResult::OAuthRequired { .. } => {
214+
panic!("Unexpected OAuthRequired for docker stdio transport")
215+
}
216+
}
217+
}
218+
173219
/// Verify that environment variables are passed through correctly
174220
/// when platform flags are applied (important because CREATE_NO_WINDOW
175221
/// is OR'd with CREATE_UNICODE_ENVIRONMENT internally).

0 commit comments

Comments
 (0)