diff --git a/crates/goose-acp/src/server.rs b/crates/goose-acp/src/server.rs
index 8c1e0197..4a12d204 100644
--- a/crates/goose-acp/src/server.rs
+++ b/crates/goose-acp/src/server.rs
@@ -31,7 +31,7 @@ use sacp::schema::{
ConfigOptionUpdate, Content, ContentBlock, ContentChunk, CurrentModeUpdate, EmbeddedResource,
EmbeddedResourceResource, FileSystemCapabilities, ImageContent, InitializeRequest,
InitializeResponse, ListSessionsRequest, ListSessionsResponse, LoadSessionRequest,
- LoadSessionResponse, McpCapabilities, McpServer, ModelId, ModelInfo, NewSessionRequest,
+ LoadSessionResponse, McpCapabilities, McpServer, Meta, ModelId, ModelInfo, NewSessionRequest,
NewSessionResponse, PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest,
PromptResponse, RequestPermissionOutcome, RequestPermissionRequest, ResourceLink,
SessionCapabilities, SessionCloseCapabilities, SessionConfigOption,
@@ -90,34 +90,46 @@ pub struct GooseAcpAgent {
disable_session_naming: bool,
}
+fn extract_timeout_from_meta(meta: &Option) -> Option {
+ meta.as_ref()
+ .and_then(|m| m.get("timeout"))
+ .and_then(|v| v.as_u64())
+}
+
fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result {
match mcp_server {
- McpServer::Stdio(stdio) => Ok(ExtensionConfig::Stdio {
- name: stdio.name,
- description: String::new(),
- cmd: stdio.command.to_string_lossy().to_string(),
- args: stdio.args,
- envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()),
- env_keys: vec![],
- timeout: None,
- bundled: Some(false),
- available_tools: vec![],
- }),
- McpServer::Http(http) => Ok(ExtensionConfig::StreamableHttp {
- name: http.name,
- description: String::new(),
- uri: http.url,
- envs: Envs::default(),
- env_keys: vec![],
- headers: http
- .headers
- .into_iter()
- .map(|h| (h.name, h.value))
- .collect(),
- timeout: None,
- bundled: Some(false),
- available_tools: vec![],
- }),
+ McpServer::Stdio(stdio) => {
+ let timeout = extract_timeout_from_meta(&stdio.meta);
+ Ok(ExtensionConfig::Stdio {
+ name: stdio.name,
+ description: String::new(),
+ cmd: stdio.command.to_string_lossy().to_string(),
+ args: stdio.args,
+ envs: Envs::new(stdio.env.into_iter().map(|e| (e.name, e.value)).collect()),
+ env_keys: vec![],
+ timeout,
+ bundled: Some(false),
+ available_tools: vec![],
+ })
+ }
+ McpServer::Http(http) => {
+ let timeout = extract_timeout_from_meta(&http.meta);
+ Ok(ExtensionConfig::StreamableHttp {
+ name: http.name,
+ description: String::new(),
+ uri: http.url,
+ envs: Envs::default(),
+ env_keys: vec![],
+ headers: http
+ .headers
+ .into_iter()
+ .map(|h| (h.name, h.value))
+ .collect(),
+ timeout,
+ bundled: Some(false),
+ available_tools: vec![],
+ })
+ }
McpServer::Sse(_) => Err("SSE is unsupported, migrate to streamable_http".to_string()),
_ => Err("Unknown MCP server type".to_string()),
}
diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs
index bee36018..6f80e976 100644
--- a/crates/goose/src/agents/extension_manager.rs
+++ b/crates/goose/src/agents/extension_manager.rs
@@ -58,6 +58,14 @@ static RE_ENV_BRACES: Lazy =
static RE_ENV_SIMPLE: Lazy =
Lazy::new(|| regex::Regex::new(r"\$([A-Za-z_][A-Za-z0-9_]*)").expect("valid regex"));
+fn resolve_timeout(timeout: Option) -> u64 {
+ timeout.unwrap_or_else(|| {
+ Config::global()
+ .get_goose_default_extension_timeout()
+ .unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)
+ })
+}
+
struct Extension {
pub config: ExtensionConfig,
/// Resolved config snapshot (with secrets from keyring substituted)
@@ -275,7 +283,7 @@ async fn child_process_client(
let client_result = McpClient::connect_with_container(
transport,
- Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT)),
+ Duration::from_secs(resolve_timeout(*timeout)),
provider,
docker_container,
client_name,
@@ -441,8 +449,7 @@ async fn create_streamable_http_client(
},
);
- let timeout_duration =
- Duration::from_secs(timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT));
+ let timeout_duration = Duration::from_secs(resolve_timeout(timeout));
let client_res = McpClient::connect(
transport,
@@ -636,7 +643,7 @@ impl ExtensionManager {
(def.client_factory)(context)
} else {
// Builtin MCP server extension
- let timeout_secs = timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT);
+ let timeout_secs = resolve_timeout(timeout);
let extension_fn =
get_builtin_extension(normalized_name.as_str()).ok_or_else(|| {
ExtensionError::ConfigError(format!("Unknown extension: {}", name))
diff --git a/crates/goose/src/config/base.rs b/crates/goose/src/config/base.rs
index d2dd2ce0..b6698b5a 100644
--- a/crates/goose/src/config/base.rs
+++ b/crates/goose/src/config/base.rs
@@ -1064,6 +1064,7 @@ config_value!(GEMINI3_THINKING_LEVEL, String);
config_value!(CLAUDE_THINKING_TYPE, String);
config_value!(CLAUDE_THINKING_EFFORT, String);
config_value!(CLAUDE_THINKING_BUDGET, i32);
+config_value!(GOOSE_DEFAULT_EXTENSION_TIMEOUT, u64);
fn find_workspace_or_exe_root() -> Option {
let exe = std::env::current_exe().ok()?;