Skip to content

Commit 7657384

Browse files
authored
fix(mcp): guard against duplicate serve() on streamable-http MCP servers (#8435)
* fix(mcp): guard against duplicate serve() on streamable-http servers Remote streamable-http MCP servers (type "http") intermittently failed to connect with `400 Bad Request ... when send initialize request`, or connected then dropped a few seconds later. Root cause: `start_mcp_server` had no idempotency guard, so a server could be serve()'d twice (e.g. a rename fires activate + syncServersAndRestart, and `restart_active_mcp_servers` restarts active servers without clearing them first). Each serve() spins up an independent rmcp client that sends its own `initialize` (both with request id 0); the second is rejected by the server as a duplicate on an already-initialized session, tearing down the connection. Fix: make `start_mcp_server` idempotent — skip if the server is already running or a start is already in flight (tracked via a new `mcp_starting` set on AppState), clearing the in-flight marker when the attempt completes. The reconnect path (`schedule_mcp_start_task`) is intentionally left unguarded so auto-reconnect can still re-serve. Fixes #8411 * fix(mcp): log duplicate-start skips at debug level Address review: the idempotency-guard skip messages are routine, not noteworthy, so log them at debug instead of info.
1 parent befb51c commit 7657384

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

src-tauri/src/core/mcp/helpers.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,20 @@ pub async fn start_mcp_server<R: Runtime>(
330330
let app_state = app.state::<AppState>();
331331
let active_servers_state = app_state.mcp_active_servers.clone();
332332

333+
// Idempotency guard: never serve() the same server twice. Two serve() calls
334+
// spin up two independent clients that each send an `initialize` (both with
335+
// request id 0); the second is rejected by streamable-http servers with a
336+
// 400, tearing down the connection (issue #8411). This can happen when boot
337+
// startup and a frontend activation fire for the same server.
338+
if servers_state.lock().await.contains_key(&name) {
339+
log::debug!("MCP server {name} already running; skipping duplicate start");
340+
return Ok(());
341+
}
342+
if !app_state.mcp_starting.lock().await.insert(name.clone()) {
343+
log::debug!("MCP server {name} start already in progress; skipping duplicate start");
344+
return Ok(());
345+
}
346+
333347
// Store active server config for restart purposes
334348
store_active_server_config(&active_servers_state, &name, &config).await;
335349

@@ -343,6 +357,10 @@ pub async fn start_mcp_server<R: Runtime>(
343357
)
344358
.await;
345359

360+
// Start attempt finished (success or failure) — clear the in-flight marker so
361+
// future (re)activations aren't blocked.
362+
app_state.mcp_starting.lock().await.remove(&name);
363+
346364
match first_start_result {
347365
Ok(_) => {
348366
log::info!("MCP server {name} started successfully");

src-tauri/src/core/state.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
use std::{collections::HashMap, sync::Arc};
1+
use std::{
2+
collections::{HashMap, HashSet},
3+
sync::Arc,
4+
};
25

36
use crate::core::{
47
downloads::models::DownloadManagerState,
@@ -67,6 +70,10 @@ pub struct AppState {
6770
pub mcp_settings: Arc<Mutex<McpSettings>>,
6871
pub mcp_shutdown_in_progress: Arc<Mutex<bool>>,
6972
pub mcp_monitoring_tasks: Arc<Mutex<HashMap<String, tauri::async_runtime::JoinHandle<()>>>>,
73+
/// Names of MCP servers whose initial start is currently in flight. Guards
74+
/// against a server being `serve()`'d twice (e.g. boot startup racing a
75+
/// frontend activation), which sends duplicate `initialize` requests.
76+
pub mcp_starting: Arc<Mutex<HashSet<String>>>,
7077
pub background_cleanup_handle: Arc<Mutex<Option<tokio::task::JoinHandle<()>>>>,
7178
pub mcp_server_pids: Arc<Mutex<HashMap<String, u32>>>,
7279
/// Remote provider configurations (e.g., Anthropic, OpenAI, etc.)
@@ -97,6 +104,7 @@ impl Default for AppState {
97104
mcp_settings: Default::default(),
98105
mcp_shutdown_in_progress: Default::default(),
99106
mcp_monitoring_tasks: Default::default(),
107+
mcp_starting: Default::default(),
100108
background_cleanup_handle: Default::default(),
101109
mcp_server_pids: Default::default(),
102110
provider_configs: Default::default(),

src-tauri/src/lib.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ use core::{
1212
#[cfg(not(feature = "cli"))]
1313
use jan_utils::generate_app_token;
1414
#[cfg(not(feature = "cli"))]
15-
use std::{collections::HashMap, sync::Arc};
15+
use std::{
16+
collections::{HashMap, HashSet},
17+
sync::Arc,
18+
};
1619
#[cfg(not(feature = "cli"))]
1720
use tauri::{Emitter, Manager, RunEvent};
1821
#[cfg(not(feature = "cli"))]
@@ -272,6 +275,7 @@ pub fn run() {
272275
mcp_settings: Arc::new(Mutex::new(McpSettings::default())),
273276
mcp_shutdown_in_progress: Arc::new(Mutex::new(false)),
274277
mcp_monitoring_tasks: Arc::new(Mutex::new(HashMap::new())),
278+
mcp_starting: Arc::new(Mutex::new(HashSet::new())),
275279
background_cleanup_handle: Arc::new(Mutex::new(None)),
276280
mcp_server_pids: Arc::new(Mutex::new(HashMap::new())),
277281
provider_configs: Arc::new(Mutex::new(HashMap::new())),

0 commit comments

Comments
 (0)