From 4c2fbbaddec8357291261f3e618ba34f269e9a64 Mon Sep 17 00:00:00 2001 From: "John D. Swanson" Date: Thu, 18 Dec 2025 16:05:07 -0500 Subject: [PATCH] Add global and per-server timeout settings for MCP context servers - Add global context_server_timeout setting (default 60000ms) - Add per-server timeout field to HTTP context server configuration - Implement timeout precedence: per-server > global > 60s default - Both Stdio and HTTP servers now support timeout configuration - Update ContextServer to pass timeout to HTTP transport - Document timeout settings with examples in default.json --- assets/settings/default.json | 17 +++++++ crates/agent_servers/src/acp.rs | 1 + .../configure_context_server_modal.rs | 2 + crates/context_server/src/context_server.rs | 17 ++++++- crates/project/src/context_server_store.rs | 44 +++++++++++++++---- crates/project/src/project_settings.rs | 10 +++++ .../settings/src/settings_content/project.rs | 8 ++++ crates/settings/src/vscode_import.rs | 1 + 8 files changed, 90 insertions(+), 10 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 154fe2d6e34e6573e95e7ffedbb46df8bbf10634..3bbf275aaf835b9549e36bd43b68fd4d4a64bce8 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -2253,6 +2253,23 @@ "ssh_connections": [], // Whether to read ~/.ssh/config for ssh connection sources. "read_ssh_config": true, + // Default timeout in milliseconds for all context server tool calls. + // Individual servers can override this in their configuration. + // Examples: + // "context_servers": { + // "my-stdio-server": { + // "command": "/path/to/server", + // "args": ["--flag"], + // "timeout": 120000 // Override: 2 minutes for this server + // }, + // "my-http-server": { + // "url": "https://example.com/mcp", + // "headers": { "Authorization": "Bearer token" }, + // "timeout": 90000 // Override: 90 seconds for this server + // } + // } + // Default: 60000 (60 seconds) + "context_server_timeout": 60000, // Configures context servers for use by the agent. "context_servers": {}, // Configures agent servers available in the agent panel. diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs index e99855fe8a7241468e93f01fe6c7b6fee161f600..325bdd98d325f7f4dac5d419dadb6a4fbc87c04f 100644 --- a/crates/agent_servers/src/acp.rs +++ b/crates/agent_servers/src/acp.rs @@ -286,6 +286,7 @@ impl AgentConnection for AcpConnection { project::context_server_store::ContextServerConfiguration::Http { url, headers, + timeout: _, } => Some(acp::McpServer::Http( acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers( headers diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index b30f1494f0d4dcbf3ef63cc7f549d16374f4899b..f37da34b8a735efaa9c07eab822e35a819691edf 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -172,6 +172,7 @@ impl ConfigurationSource { enabled: true, url, headers: auth, + timeout: None, }, ) }) @@ -411,6 +412,7 @@ impl ConfigureContextServerModal { enabled: _, url, headers, + timeout: _, } => Some(ConfigurationTarget::ExistingHttp { id: server_id, url, diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 92804549c69b01dd3729efb3a0b47905cd73d813..2a4b749d9aae00c12d71da7f0fa3308cf65a14b9 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -10,6 +10,7 @@ use collections::HashMap; use http_client::HttpClient; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use std::{fmt::Display, path::PathBuf}; use anyhow::Result; @@ -39,6 +40,7 @@ pub struct ContextServer { id: ContextServerId, client: RwLock>>, configuration: ContextServerTransport, + request_timeout: Option, } impl ContextServer { @@ -54,6 +56,7 @@ impl ContextServer { command, working_directory.map(|directory| directory.to_path_buf()), ), + request_timeout: None, // Stdio handles timeout through command } } @@ -63,6 +66,7 @@ impl ContextServer { headers: HashMap, http_client: Arc, executor: gpui::BackgroundExecutor, + request_timeout: Option, ) -> Result { let transport = match endpoint.scheme() { "http" | "https" => { @@ -73,14 +77,23 @@ impl ContextServer { } _ => anyhow::bail!("unsupported MCP url scheme {}", endpoint.scheme()), }; - Ok(Self::new(id, transport)) + Ok(Self::new_with_timeout(id, transport, request_timeout)) } pub fn new(id: ContextServerId, transport: Arc) -> Self { + Self::new_with_timeout(id, transport, None) + } + + pub fn new_with_timeout( + id: ContextServerId, + transport: Arc, + request_timeout: Option, + ) -> Self { Self { id, client: RwLock::new(None), configuration: ContextServerTransport::Custom(transport), + request_timeout, } } @@ -113,7 +126,7 @@ impl ContextServer { client::ContextServerId(self.id.0.clone()), self.id().0, transport.clone(), - None, + self.request_timeout, cx.clone(), )?, }) diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 7ba46a46872ba57c758baccf9f67b0039818ee75..683e8c05444e557f8a6fb42c522721bd70d1a1f6 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -2,6 +2,7 @@ pub mod extension; pub mod registry; use std::sync::Arc; +use std::time::Duration; use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet}; @@ -102,6 +103,7 @@ pub enum ContextServerConfiguration { Http { url: url::Url, headers: HashMap, + timeout: Option, }, } @@ -151,9 +153,14 @@ impl ContextServerConfiguration { enabled: _, url, headers: auth, + timeout, } => { let url = url::Url::parse(&url).log_err()?; - Some(ContextServerConfiguration::Http { url, headers: auth }) + Some(ContextServerConfiguration::Http { + url, + headers: auth, + timeout, + }) } } } @@ -482,18 +489,31 @@ impl ContextServerStore { configuration: Arc, cx: &mut Context, ) -> Result> { + // Get global timeout from settings + let global_timeout = ProjectSettings::get_global(cx).context_server_timeout; + if let Some(factory) = self.context_server_factory.as_ref() { return Ok(factory(id, configuration)); } match configuration.as_ref() { - ContextServerConfiguration::Http { url, headers } => Ok(Arc::new(ContextServer::http( - id, + ContextServerConfiguration::Http { url, - headers.clone(), - cx.http_client(), - cx.background_executor().clone(), - )?)), + headers, + timeout, + } => { + // Apply timeout precedence for HTTP servers: per-server > global + let resolved_timeout = timeout.unwrap_or(global_timeout); + + Ok(Arc::new(ContextServer::http( + id, + url, + headers.clone(), + cx.http_client(), + cx.background_executor().clone(), + Some(Duration::from_millis(resolved_timeout)), + )?)) + } _ => { let root_path = self .project @@ -511,9 +531,16 @@ impl ContextServerStore { }) }) }); + + // Apply timeout precedence for stdio servers: per-server > global + let mut command_with_timeout = configuration.command().unwrap().clone(); + if command_with_timeout.timeout.is_none() { + command_with_timeout.timeout = Some(global_timeout); + } + Ok(Arc::new(ContextServer::stdio( id, - configuration.command().unwrap().clone(), + command_with_timeout, root_path, ))) } @@ -1257,6 +1284,7 @@ mod tests { enabled: true, url: server_url.to_string(), headers: Default::default(), + timeout: None, }, )], ) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 633f2bbd3b40139f6355e109211d665cfd0c1e5f..3d5cd23001cc29c1951112a1637344cbb3087fd7 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -59,6 +59,9 @@ pub struct ProjectSettings { /// Settings for context servers used for AI-related features. pub context_servers: HashMap, ContextServerSettings>, + /// Default timeout for context server requests in milliseconds. + pub context_server_timeout: u64, + /// Configuration for Diagnostics-related features. pub diagnostics: DiagnosticsSettings, @@ -141,6 +144,8 @@ pub enum ContextServerSettings { /// Optional authentication configuration for the remote server. #[serde(skip_serializing_if = "HashMap::is_empty", default)] headers: HashMap, + /// Timeout for tool calls in milliseconds. + timeout: Option, }, Extension { /// Whether the context server is enabled. @@ -167,10 +172,12 @@ impl From for ContextServerSettings { enabled, url, headers, + timeout, } => ContextServerSettings::Http { enabled, url, headers, + timeout, }, } } @@ -188,10 +195,12 @@ impl Into for ContextServerSettings { enabled, url, headers, + timeout, } => settings::ContextServerSettingsContent::Http { enabled, url, headers, + timeout, }, } } @@ -560,6 +569,7 @@ impl Settings for ProjectSettings { .into_iter() .map(|(key, value)| (key, value.into())) .collect(), + context_server_timeout: project.context_server_timeout.unwrap_or(60000), lsp: project .lsp .clone() diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs index 8e2d864149c9ecb6ca38ca73ef58205f588dc07b..791bb03481df85f20bc8778d7fbf125e2ab23dcd 100644 --- a/crates/settings/src/settings_content/project.rs +++ b/crates/settings/src/settings_content/project.rs @@ -41,6 +41,12 @@ pub struct ProjectSettingsContent { #[serde(default)] pub context_servers: HashMap, ContextServerSettingsContent>, + /// Default timeout in milliseconds for context server tool calls. + /// Can be overridden per-server in context_servers configuration. + /// + /// Default: 60000 (60 seconds) + pub context_server_timeout: Option, + /// Configuration for how direnv configuration should be loaded pub load_direnv: Option, @@ -215,6 +221,8 @@ pub enum ContextServerSettingsContent { /// Optional headers to send. #[serde(skip_serializing_if = "HashMap::is_empty", default)] headers: HashMap, + /// Timeout for tool calls in milliseconds. Defaults to global context_server_timeout if not specified. + timeout: Option, }, Extension { /// Whether the context server is enabled. diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index d77754f611e8eb1746ee9061ce5b5e1dfdbdafdb..995868713476eacd05d7350f32bf189a20b01213 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -402,6 +402,7 @@ impl VsCodeSettings { terminal: None, dap: Default::default(), context_servers: self.context_servers(), + context_server_timeout: None, load_direnv: None, slash_commands: None, git_hosting_providers: None,