Compare commits
3 Commits
lsp-indivi
...
global-and
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
548df67759 | ||
|
|
a78647f8ae | ||
|
|
4c2fbbadde |
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
)?,
|
||||
})
|
||||
|
||||
@@ -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};
|
||||
@@ -18,6 +19,10 @@ use crate::{
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
||||
/// Maximum timeout for context server requests (10 minutes).
|
||||
/// Prevents extremely large timeout values from tying up resources indefinitely.
|
||||
const MAX_TIMEOUT_MS: u64 = 600_000;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
extension::init(cx);
|
||||
}
|
||||
@@ -102,6 +107,7 @@ pub enum ContextServerConfiguration {
|
||||
Http {
|
||||
url: url::Url,
|
||||
headers: HashMap<String, String>,
|
||||
timeout: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -151,9 +157,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 +493,32 @@ 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
|
||||
// Cap at MAX_TIMEOUT_MS to prevent extremely large timeout values
|
||||
let resolved_timeout = timeout.unwrap_or(global_timeout).min(MAX_TIMEOUT_MS);
|
||||
|
||||
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 +536,23 @@ impl ContextServerStore {
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// Apply timeout precedence for stdio servers: per-server > global
|
||||
// Cap at MAX_TIMEOUT_MS to prevent extremely large timeout values
|
||||
let mut command_with_timeout = configuration
|
||||
.command()
|
||||
.context("Missing command configuration for stdio context server")?
|
||||
.clone();
|
||||
if command_with_timeout.timeout.is_none() {
|
||||
command_with_timeout.timeout = Some(global_timeout.min(MAX_TIMEOUT_MS));
|
||||
} else {
|
||||
command_with_timeout.timeout =
|
||||
command_with_timeout.timeout.map(|t| t.min(MAX_TIMEOUT_MS));
|
||||
}
|
||||
|
||||
Ok(Arc::new(ContextServer::stdio(
|
||||
id,
|
||||
configuration.command().unwrap().clone(),
|
||||
command_with_timeout,
|
||||
root_path,
|
||||
)))
|
||||
}
|
||||
@@ -1257,6 +1296,7 @@ mod tests {
|
||||
enabled: true,
|
||||
url: server_url.to_string(),
|
||||
headers: Default::default(),
|
||||
timeout: None,
|
||||
},
|
||||
)],
|
||||
)
|
||||
@@ -1327,6 +1367,165 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_server_global_timeout(cx: &mut TestAppContext) {
|
||||
// Configure global timeout to 90 seconds
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(r#"{"context_server_timeout": 90000}"#, cx)
|
||||
.expect("Failed to set test user settings");
|
||||
});
|
||||
});
|
||||
|
||||
let (_fs, project) = setup_context_server_test(cx, json!({"code.rs": ""}), vec![]).await;
|
||||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Test that create_context_server applies global timeout
|
||||
let result = store.update(cx, |store, cx| {
|
||||
store.create_context_server(
|
||||
ContextServerId("test-server".into()),
|
||||
Arc::new(ContextServerConfiguration::Http {
|
||||
url: url::Url::parse("http://localhost:8080")
|
||||
.expect("Failed to parse test URL"),
|
||||
headers: Default::default(),
|
||||
timeout: None, // Should use global timeout of 90 seconds
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Server should be created successfully with global timeout"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_server_per_server_timeout_override(cx: &mut TestAppContext) {
|
||||
const SERVER_ID: &str = "test-server";
|
||||
|
||||
// Configure global timeout to 60 seconds
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store
|
||||
.set_user_settings(r#"{"context_server_timeout": 60000}"#, cx)
|
||||
.expect("Failed to set test user settings");
|
||||
});
|
||||
});
|
||||
|
||||
let (_fs, project) = setup_context_server_test(
|
||||
cx,
|
||||
json!({"code.rs": ""}),
|
||||
vec![(
|
||||
SERVER_ID.into(),
|
||||
ContextServerSettings::Http {
|
||||
enabled: true,
|
||||
url: "http://localhost:8080".to_string(),
|
||||
headers: Default::default(),
|
||||
timeout: Some(120000), // Override to 120 seconds
|
||||
},
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Test that create_context_server applies per-server timeout override
|
||||
let result = store.update(cx, |store, cx| {
|
||||
store.create_context_server(
|
||||
ContextServerId("test-server".into()),
|
||||
Arc::new(ContextServerConfiguration::Http {
|
||||
url: url::Url::parse("http://localhost:8080")
|
||||
.expect("Failed to parse test URL"),
|
||||
headers: Default::default(),
|
||||
timeout: Some(120000), // Override: should use 120 seconds, not global 60
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Server should be created successfully with per-server timeout override"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_context_server_stdio_timeout(cx: &mut TestAppContext) {
|
||||
const SERVER_ID: &str = "stdio-server";
|
||||
|
||||
let (_fs, project) = setup_context_server_test(
|
||||
cx,
|
||||
json!({"code.rs": ""}),
|
||||
vec![(
|
||||
SERVER_ID.into(),
|
||||
ContextServerSettings::Stdio {
|
||||
enabled: true,
|
||||
command: ContextServerCommand {
|
||||
path: "/usr/bin/node".into(),
|
||||
args: vec!["server.js".into()],
|
||||
env: None,
|
||||
timeout: Some(180000), // 3 minutes
|
||||
},
|
||||
},
|
||||
)],
|
||||
)
|
||||
.await;
|
||||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Test that create_context_server works with stdio timeout
|
||||
let result = store.update(cx, |store, cx| {
|
||||
store.create_context_server(
|
||||
ContextServerId("stdio-server".into()),
|
||||
Arc::new(ContextServerConfiguration::Custom {
|
||||
command: ContextServerCommand {
|
||||
path: "/usr/bin/node".into(),
|
||||
args: vec!["server.js".into()],
|
||||
env: None,
|
||||
timeout: Some(180000), // 3 minutes
|
||||
},
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Stdio server should be created successfully with timeout"
|
||||
);
|
||||
}
|
||||
|
||||
fn dummy_server_settings() -> ContextServerSettings {
|
||||
ContextServerSettings::Stdio {
|
||||
enabled: true,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user