Skip to content

Commit 0369d21

Browse files
committed
MCP: Fix stale state after server crash
- Extract `is_port_available` and `find_available_port` into shared `net.rs` module, removing duplicate in `mcp/server.rs` and making `commands/settings.rs` a thin pass-through - Use `MCP_ACTUAL_PORT` as the single source of truth for `is_mcp_running()` instead of checking handle existence (stale handles from crashed tasks returned false positives) - Reset `MCP_ACTUAL_PORT` to 0 when the server task exits for any reason (crash or graceful shutdown), so the frontend always sees the correct state
1 parent d69f876 commit 0369d21

3 files changed

Lines changed: 10 additions & 23 deletions

File tree

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
//! Settings-related commands.
22
3-
use std::net::TcpListener;
4-
53
use tauri::{AppHandle, Manager};
64

75
use crate::file_system::update_debounce_ms;
@@ -16,20 +14,14 @@ use crate::network::mdns_discovery::update_resolve_timeout;
1614
/// Check if a port is available for binding.
1715
#[tauri::command]
1816
pub fn check_port_available(port: u16) -> bool {
19-
TcpListener::bind(("127.0.0.1", port)).is_ok()
17+
crate::net::is_port_available(port)
2018
}
2119

2220
/// Find an available port starting from the given port.
2321
/// Scans up to 100 ports from the start port.
2422
#[tauri::command]
2523
pub fn find_available_port(start_port: u16) -> Option<u16> {
26-
for offset in 0..100 {
27-
let port = start_port.saturating_add(offset);
28-
if check_port_available(port) {
29-
return Some(port);
30-
}
31-
}
32-
None
24+
crate::net::find_available_port(start_port)
3325
}
3426

3527
/// Updates the file watcher debounce duration in milliseconds.

apps/desktop/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ mod linux_icons;
9494
mod macos_icons;
9595
mod mcp;
9696
mod menu;
97+
mod net;
9798
#[cfg(any(target_os = "macos", target_os = "linux"))]
9899
mod mtp;
99100
#[cfg(any(target_os = "macos", target_os = "linux"))]

apps/desktop/src-tauri/src/mcp/server.rs

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,6 @@ impl<R: Runtime> McpState<R> {
6161
}
6262
}
6363

64-
/// Find an available port starting from `start_port`, scanning up to 100 ports.
65-
fn find_available_port(start_port: u16) -> Option<u16> {
66-
for offset in 0..100 {
67-
let port = start_port.saturating_add(offset);
68-
if std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() {
69-
return Some(port);
70-
}
71-
}
72-
None
73-
}
74-
7564
/// Start the MCP server. Binds to the configured port and spawns the server task.
7665
/// If the configured port is taken, auto-probes upward to find the next available port.
7766
pub async fn start_mcp_server<R: Runtime + 'static>(app: AppHandle<R>, config: McpConfig) -> Result<(), String> {
@@ -89,7 +78,7 @@ pub async fn start_mcp_server<R: Runtime + 'static>(app: AppHandle<R>, config: M
8978
let configured_port = config.port;
9079

9180
// Auto-probe: if the configured port is taken, find the next available one
92-
let port = find_available_port(configured_port)
81+
let port = crate::net::find_available_port(configured_port)
9382
.ok_or_else(|| format!("No available port found starting from {}.", configured_port))?;
9483
if port != configured_port {
9584
log::info!(
@@ -126,6 +115,9 @@ pub async fn start_mcp_server<R: Runtime + 'static>(app: AppHandle<R>, config: M
126115
if let Err(e) = axum::serve(listener, router).await {
127116
log::error!("MCP server crashed: {}", e);
128117
}
118+
// Server exited (crash or graceful shutdown) — reset port so
119+
// is_mcp_running() and get_mcp_actual_port() reflect reality.
120+
MCP_ACTUAL_PORT.store(0, Ordering::Relaxed);
129121
});
130122

131123
if let Ok(mut guard) = MCP_HANDLE.lock() {
@@ -157,8 +149,10 @@ pub fn stop_mcp_server() {
157149
}
158150

159151
/// Returns whether the MCP server task is currently running.
152+
/// Uses `MCP_ACTUAL_PORT` as the source of truth — the spawned task resets it
153+
/// to 0 when it exits (crash or graceful shutdown), so a non-zero port means running.
160154
pub fn is_mcp_running() -> bool {
161-
MCP_HANDLE.lock().ok().is_some_and(|guard| guard.is_some())
155+
MCP_ACTUAL_PORT.load(Ordering::Relaxed) != 0
162156
}
163157

164158
/// Returns the port the MCP server is actually listening on, or `None` if not running.

0 commit comments

Comments
 (0)