Add global and per-server timeout settings for MCP context servers

John D. Swanson created

- 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

Change summary

assets/settings/default.json                                              | 17 
crates/agent_servers/src/acp.rs                                           |  1 
crates/agent_ui/src/agent_configuration/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 
crates/settings/src/settings_content/project.rs                           |  8 
crates/settings/src/vscode_import.rs                                      |  1 
8 files changed, 90 insertions(+), 10 deletions(-)

Detailed changes

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.

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

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<Option<Arc<crate::protocol::InitializedContextServerProtocol>>>,
     configuration: ContextServerTransport,
+    request_timeout: Option<Duration>,
 }
 
 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<String, String>,
         http_client: Arc<dyn HttpClient>,
         executor: gpui::BackgroundExecutor,
+        request_timeout: Option<Duration>,
     ) -> Result<Self> {
         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<dyn crate::transport::Transport>) -> Self {
+        Self::new_with_timeout(id, transport, None)
+    }
+
+    pub fn new_with_timeout(
+        id: ContextServerId,
+        transport: Arc<dyn crate::transport::Transport>,
+        request_timeout: Option<Duration>,
+    ) -> 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(),
             )?,
         })

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<String, String>,
+        timeout: Option<u64>,
     },
 }
 
@@ -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<ContextServerConfiguration>,
         cx: &mut Context<Self>,
     ) -> Result<Arc<ContextServer>> {
+        // 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,
                 },
             )],
         )

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<Arc<str>, 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<String, String>,
+        /// Timeout for tool calls in milliseconds.
+        timeout: Option<u64>,
     },
     Extension {
         /// Whether the context server is enabled.
@@ -167,10 +172,12 @@ impl From<settings::ContextServerSettingsContent> for ContextServerSettings {
                 enabled,
                 url,
                 headers,
+                timeout,
             } => ContextServerSettings::Http {
                 enabled,
                 url,
                 headers,
+                timeout,
             },
         }
     }
@@ -188,10 +195,12 @@ impl Into<settings::ContextServerSettingsContent> 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()

crates/settings/src/settings_content/project.rs 🔗

@@ -41,6 +41,12 @@ pub struct ProjectSettingsContent {
     #[serde(default)]
     pub context_servers: HashMap<Arc<str>, 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<u64>,
+
     /// Configuration for how direnv configuration should be loaded
     pub load_direnv: Option<DirenvSettings>,
 
@@ -215,6 +221,8 @@ pub enum ContextServerSettingsContent {
         /// Optional headers to send.
         #[serde(skip_serializing_if = "HashMap::is_empty", default)]
         headers: HashMap<String, String>,
+        /// Timeout for tool calls in milliseconds. Defaults to global context_server_timeout if not specified.
+        timeout: Option<u64>,
     },
     Extension {
         /// Whether the context server is enabled.

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,