feat: Adding streamable-http transport support for backend, desktop and cli (#2942)

This commit is contained in:
btdeviant
2025-07-01 16:35:11 -07:00
committed by GitHub
parent 2cadc1db7e
commit 2948d4a375
21 changed files with 1085 additions and 21 deletions
+44 -2
View File
@@ -15,7 +15,7 @@ use crate::config::permission::PermissionLevel;
#[derive(Error, Debug)]
pub enum ExtensionError {
#[error("Failed to start the MCP server from configuration `{0}` `{1}`")]
Initialization(ExtensionConfig, ClientError),
Initialization(Box<ExtensionConfig>, ClientError),
#[error("Failed a client call to an MCP server: {0}")]
Client(#[from] ClientError),
#[error("User Message exceeded context-limit. History could not be truncated to accommodate.")]
@@ -54,7 +54,7 @@ impl Envs {
"LD_AUDIT", // Loads a monitoring library that can intercept execution
"LD_DEBUG", // Enables verbose linker logging (information disclosure risk)
"LD_BIND_NOW", // Forces immediate symbol resolution, affecting ASLR
"LD_ASSUME_KERNEL", // Tricks linker into thinking its running on an older kernel
"LD_ASSUME_KERNEL", // Tricks linker into thinking it's running on an older kernel
// 🍎 macOS dynamic linker variables
"DYLD_LIBRARY_PATH", // Same as LD_LIBRARY_PATH but for macOS
"DYLD_INSERT_LIBRARIES", // macOS equivalent of LD_PRELOAD
@@ -168,6 +168,26 @@ pub enum ExtensionConfig {
#[serde(default)]
bundled: Option<bool>,
},
/// Streamable HTTP client with a URI endpoint using MCP Streamable HTTP specification
#[serde(rename = "streamable_http")]
StreamableHttp {
/// The name used to identify this extension
name: String,
uri: String,
#[serde(default)]
envs: Envs,
#[serde(default)]
env_keys: Vec<String>,
#[serde(default)]
headers: HashMap<String, String>,
description: Option<String>,
// NOTE: set timeout to be optional for compatibility.
// However, new configurations should include this field.
timeout: Option<u64>,
/// Whether this extension is bundled with Goose
#[serde(default)]
bundled: Option<bool>,
},
/// Frontend-provided tools that will be called through the frontend
#[serde(rename = "frontend")]
Frontend {
@@ -207,6 +227,24 @@ impl ExtensionConfig {
}
}
pub fn streamable_http<S: Into<String>, T: Into<u64>>(
name: S,
uri: S,
description: S,
timeout: T,
) -> Self {
Self::StreamableHttp {
name: name.into(),
uri: uri.into(),
envs: Envs::default(),
env_keys: Vec::new(),
headers: HashMap::new(),
description: Some(description.into()),
timeout: Some(timeout.into()),
bundled: None,
}
}
pub fn stdio<S: Into<String>, T: Into<u64>>(
name: S,
cmd: S,
@@ -263,6 +301,7 @@ impl ExtensionConfig {
pub fn name(&self) -> String {
match self {
Self::Sse { name, .. } => name,
Self::StreamableHttp { name, .. } => name,
Self::Stdio { name, .. } => name,
Self::Builtin { name, .. } => name,
Self::Frontend { name, .. } => name,
@@ -275,6 +314,9 @@ impl std::fmt::Display for ExtensionConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtensionConfig::Sse { name, uri, .. } => write!(f, "SSE({}: {})", name, uri),
ExtensionConfig::StreamableHttp { name, uri, .. } => {
write!(f, "StreamableHttp({}: {})", name, uri)
}
ExtensionConfig::Stdio {
name, cmd, args, ..
} => {
+28 -3
View File
@@ -18,7 +18,7 @@ use crate::agents::extension::Envs;
use crate::config::{Config, ExtensionConfigManager};
use crate::prompt_template;
use mcp_client::client::{ClientCapabilities, ClientInfo, McpClient, McpClientTrait};
use mcp_client::transport::{SseTransport, StdioTransport, Transport};
use mcp_client::transport::{SseTransport, StdioTransport, StreamableHttpTransport, Transport};
use mcp_core::{prompt::Prompt, Content, Tool, ToolCall, ToolError};
use serde_json::Value;
@@ -195,6 +195,28 @@ impl ExtensionManager {
.await?,
)
}
ExtensionConfig::StreamableHttp {
uri,
envs,
env_keys,
headers,
timeout,
..
} => {
let all_envs = merge_environments(envs, env_keys, &sanitized_name).await?;
let transport =
StreamableHttpTransport::with_headers(uri, all_envs, headers.clone());
let handle = transport.start().await?;
Box::new(
McpClient::connect(
handle,
Duration::from_secs(
timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT),
),
)
.await?,
)
}
ExtensionConfig::Stdio {
cmd,
args,
@@ -256,7 +278,7 @@ impl ExtensionManager {
let init_result = client
.initialize(info, capabilities)
.await
.map_err(|e| ExtensionError::Initialization(config.clone(), e))?;
.map_err(|e| ExtensionError::Initialization(Box::new(config.clone()), e))?;
if let Some(instructions) = init_result.instructions {
self.instructions
@@ -752,10 +774,13 @@ impl ExtensionManager {
ExtensionConfig::Sse {
description, name, ..
}
| ExtensionConfig::StreamableHttp {
description, name, ..
}
| ExtensionConfig::Stdio {
description, name, ..
} => {
// For SSE/Stdio, use description if available
// For SSE/StreamableHttp/Stdio, use description if available
description
.as_ref()
.map(|s| s.to_string())