diff --git a/Cargo.lock b/Cargo.lock index 68dc2ab48b42..f78a0b25be16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,19 +19,6 @@ dependencies = [ "cpufeatures", ] -[[package]] -name = "agent-client-protocol-schema" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16d08d095e8069115774caa50392e9c818e3fb1c482ef4f3153d26b4595482f2" -dependencies = [ - "anyhow", - "derive_more", - "schemars 1.2.0", - "serde", - "serde_json", -] - [[package]] name = "agent-client-protocol-schema" version = "0.10.5" @@ -2968,7 +2955,7 @@ dependencies = [ name = "goose" version = "1.17.0" dependencies = [ - "agent-client-protocol-schema 0.10.5", + "agent-client-protocol-schema", "ahash", "anyhow", "async-stream", @@ -3017,7 +3004,7 @@ dependencies = [ "rand 0.8.5", "regex", "reqwest 0.12.28", - "rmcp 0.9.1", + "rmcp", "sacp", "schemars 1.2.0", "serde", @@ -3069,7 +3056,7 @@ dependencies = [ "once_cell", "paste", "regex", - "rmcp 0.9.1", + "rmcp", "serde", "serde_json", "tokio", @@ -3082,7 +3069,7 @@ dependencies = [ name = "goose-cli" version = "1.17.0" dependencies = [ - "agent-client-protocol-schema 0.10.5", + "agent-client-protocol-schema", "anstream", "anyhow", "async-trait", @@ -3108,7 +3095,7 @@ dependencies = [ "open", "rand 0.8.5", "regex", - "rmcp 0.9.1", + "rmcp", "rustyline", "sacp", "serde", @@ -3164,7 +3151,7 @@ dependencies = [ "rayon", "regex", "reqwest 0.11.27", - "rmcp 0.9.1", + "rmcp", "schemars 1.2.0", "serde", "serde_json", @@ -3219,7 +3206,7 @@ dependencies = [ "http 1.4.0", "rand 0.9.2", "reqwest 0.12.28", - "rmcp 0.9.1", + "rmcp", "rustls 0.23.31", "schemars 1.2.0", "serde", @@ -5742,9 +5729,9 @@ dependencies = [ [[package]] name = "process-wrap" -version = "8.2.1" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3ef4f2f0422f23a82ec9f628ea2acd12871c81a9362b02c43c1aa86acfc3ba1" +checksum = "5e5fd83ab7fa55fd06f5e665e3fc52b8bca451c0486b8ea60ad649cd1c10a5da" dependencies = [ "futures", "indexmap 2.12.1", @@ -6239,9 +6226,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "0.9.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaa07b85b779d1e1df52dd79f6c6bffbe005b191f07290136cc42a142da3409a" +checksum = "528d42f8176e6e5e71ea69182b17d1d0a19a6b3b894b564678b74cd7cab13cfa" dependencies = [ "async-trait", "base64 0.22.1", @@ -6252,12 +6239,12 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "oauth2", - "paste", + "pastey", "pin-project-lite", "process-wrap", "rand 0.9.2", "reqwest 0.12.28", - "rmcp-macros 0.9.1", + "rmcp-macros", "schemars 1.2.0", "serde", "serde_json", @@ -6272,41 +6259,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "rmcp" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528d42f8176e6e5e71ea69182b17d1d0a19a6b3b894b564678b74cd7cab13cfa" -dependencies = [ - "async-trait", - "base64 0.22.1", - "chrono", - "futures", - "pastey", - "pin-project-lite", - "rmcp-macros 0.12.0", - "schemars 1.2.0", - "serde", - "serde_json", - "thiserror 2.0.17", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "rmcp-macros" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f6fa09933cac0d0204c8a5d647f558425538ed6a0134b1ebb1ae4dc00c96db3" -dependencies = [ - "darling 0.21.3", - "proc-macro2", - "quote", - "serde_json", - "syn 2.0.111", -] - [[package]] name = "rmcp-macros" version = "0.12.0" @@ -6555,18 +6507,18 @@ checksum = "dd29631678d6fb0903b69223673e122c32e9ae559d0960a38d574695ebc0ea15" [[package]] name = "sacp" -version = "10.0.0-alpha.3" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15a8b2266ce4c22bb4f7b7744a49dfed3890ff8c0f0df0b66794a973d639331c" +checksum = "47c1b52b3ee79933b19f2ce71945eaa17ef91ee68444e6716d05e335763af1a4" dependencies = [ - "agent-client-protocol-schema 0.6.3", + "agent-client-protocol-schema", "anyhow", "boxfnonce", "futures", "futures-concurrency", "fxhash", "jsonrpcmsg", - "rmcp 0.12.0", + "rmcp", "sacp-derive", "schemars 1.2.0", "serde", @@ -6580,9 +6532,9 @@ dependencies = [ [[package]] name = "sacp-derive" -version = "10.0.0-alpha.2" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9b8803d6b4e86f5c1dc2fb0f0a2545f8504beb93492881ca4cd9982130efd12" +checksum = "92150f9246c01d501855e34469810a82adc27c416c8d8e21665567f8cd966f29" dependencies = [ "proc-macro2", "quote", @@ -7486,7 +7438,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.56.0", + "windows 0.57.0", ] [[package]] @@ -7500,7 +7452,7 @@ dependencies = [ "memchr", "ntapi", "rayon", - "windows 0.56.0", + "windows 0.57.0", ] [[package]] @@ -8877,6 +8829,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.59.0" @@ -8921,6 +8883,18 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.59.0" @@ -8982,6 +8956,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-implement" version = "0.59.0" @@ -9015,6 +9000,17 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "windows-interface" version = "0.59.3" @@ -9733,9 +9729,9 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4a4e8e9dc5c62d159f04fcdbe07f4c3fb710415aab4754bf11505501e3251d" +checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index 490c56b12706..fc7962e918ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ uninlined_format_args = "allow" string_slice = "warn" [workspace.dependencies] -rmcp = { version = "0.9.1", features = ["schemars", "auth"] } +rmcp = { version = "0.12.0", features = ["schemars", "auth"] } +sacp = "10.0.0" webbrowser = "1.0" which = "8.0.0" etcetera = "0.11.0" diff --git a/crates/goose-bench/src/eval_suites/core/computercontroller/script.rs b/crates/goose-bench/src/eval_suites/core/computercontroller/script.rs index 92fb1353edd9..1385b7cebc17 100644 --- a/crates/goose-bench/src/eval_suites/core/computercontroller/script.rs +++ b/crates/goose-bench/src/eval_suites/core/computercontroller/script.rs @@ -78,7 +78,7 @@ impl Evaluation for ComputerControllerScript { ExtensionRequirements { builtin: vec!["computercontroller".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/computercontroller/web_scrape.rs b/crates/goose-bench/src/eval_suites/core/computercontroller/web_scrape.rs index 9dfd983de2a2..a61fd4f07238 100644 --- a/crates/goose-bench/src/eval_suites/core/computercontroller/web_scrape.rs +++ b/crates/goose-bench/src/eval_suites/core/computercontroller/web_scrape.rs @@ -81,7 +81,7 @@ impl Evaluation for ComputerControllerWebScrape { ExtensionRequirements { builtin: vec!["computercontroller".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/developer/create_file.rs b/crates/goose-bench/src/eval_suites/core/developer/create_file.rs index a230415732cc..efe0bab01e13 100644 --- a/crates/goose-bench/src/eval_suites/core/developer/create_file.rs +++ b/crates/goose-bench/src/eval_suites/core/developer/create_file.rs @@ -127,7 +127,7 @@ impl Evaluation for DeveloperCreateFile { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/developer/list_files.rs b/crates/goose-bench/src/eval_suites/core/developer/list_files.rs index ddc7db44e657..397dcfed3bae 100644 --- a/crates/goose-bench/src/eval_suites/core/developer/list_files.rs +++ b/crates/goose-bench/src/eval_suites/core/developer/list_files.rs @@ -86,7 +86,7 @@ impl Evaluation for DeveloperListFiles { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs b/crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs index 1a95c68dd934..41177fc496fb 100644 --- a/crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs +++ b/crates/goose-bench/src/eval_suites/core/developer/simple_repo_clone_test.rs @@ -224,7 +224,7 @@ impl Evaluation for SimpleRepoCloneTest { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/developer_image/image.rs b/crates/goose-bench/src/eval_suites/core/developer_image/image.rs index a9178f43f686..070decbe1636 100644 --- a/crates/goose-bench/src/eval_suites/core/developer_image/image.rs +++ b/crates/goose-bench/src/eval_suites/core/developer_image/image.rs @@ -100,7 +100,7 @@ impl Evaluation for DeveloperImage { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/developer_search_replace/search_replace.rs b/crates/goose-bench/src/eval_suites/core/developer_search_replace/search_replace.rs index 8a3deb3ea678..2c1bcf1964f0 100644 --- a/crates/goose-bench/src/eval_suites/core/developer_search_replace/search_replace.rs +++ b/crates/goose-bench/src/eval_suites/core/developer_search_replace/search_replace.rs @@ -108,7 +108,7 @@ impl Evaluation for DeveloperSearchReplace { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/core/memory/save_fact.rs b/crates/goose-bench/src/eval_suites/core/memory/save_fact.rs index d079c63cc371..f737a65d5d9c 100644 --- a/crates/goose-bench/src/eval_suites/core/memory/save_fact.rs +++ b/crates/goose-bench/src/eval_suites/core/memory/save_fact.rs @@ -83,7 +83,7 @@ impl Evaluation for MemoryRememberMemory { ExtensionRequirements { builtin: vec!["memory".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/evaluation.rs b/crates/goose-bench/src/eval_suites/evaluation.rs index cb12919810d6..3f11f6808120 100644 --- a/crates/goose-bench/src/eval_suites/evaluation.rs +++ b/crates/goose-bench/src/eval_suites/evaluation.rs @@ -36,7 +36,7 @@ pub struct EvalMetric { pub struct ExtensionRequirements { pub builtin: Vec, pub external: Vec, - pub remote: Vec, + pub streamable_http: Vec, } #[async_trait] @@ -53,7 +53,7 @@ pub trait Evaluation: Send + Sync { ExtensionRequirements { builtin: Vec::new(), external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/vibes/blog_summary.rs b/crates/goose-bench/src/eval_suites/vibes/blog_summary.rs index f3e0f22d55b1..261e3ad78d54 100644 --- a/crates/goose-bench/src/eval_suites/vibes/blog_summary.rs +++ b/crates/goose-bench/src/eval_suites/vibes/blog_summary.rs @@ -76,7 +76,7 @@ impl Evaluation for BlogSummary { ExtensionRequirements { builtin: vec!["developer".to_string()], external: vec!["uvx mcp-server-fetch".to_string()], - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/vibes/flappy_bird.rs b/crates/goose-bench/src/eval_suites/vibes/flappy_bird.rs index ea9f5fe26f6c..e271595c267b 100644 --- a/crates/goose-bench/src/eval_suites/vibes/flappy_bird.rs +++ b/crates/goose-bench/src/eval_suites/vibes/flappy_bird.rs @@ -118,7 +118,7 @@ impl Evaluation for FlappyBird { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/vibes/goose_wiki.rs b/crates/goose-bench/src/eval_suites/vibes/goose_wiki.rs index dc16d37d187e..ba4f980c026a 100644 --- a/crates/goose-bench/src/eval_suites/vibes/goose_wiki.rs +++ b/crates/goose-bench/src/eval_suites/vibes/goose_wiki.rs @@ -128,7 +128,7 @@ impl Evaluation for GooseWiki { ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/vibes/restaurant_research.rs b/crates/goose-bench/src/eval_suites/vibes/restaurant_research.rs index 92f3333c954d..7efa70e4ac41 100644 --- a/crates/goose-bench/src/eval_suites/vibes/restaurant_research.rs +++ b/crates/goose-bench/src/eval_suites/vibes/restaurant_research.rs @@ -99,7 +99,7 @@ Present the information in order of significance or quality. Focus specifically ExtensionRequirements { builtin: vec!["developer".to_string()], external: vec!["uvx mcp-server-fetch".to_string()], - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-bench/src/eval_suites/vibes/squirrel_census.rs b/crates/goose-bench/src/eval_suites/vibes/squirrel_census.rs index 21133fdc487b..49b8b743cca1 100644 --- a/crates/goose-bench/src/eval_suites/vibes/squirrel_census.rs +++ b/crates/goose-bench/src/eval_suites/vibes/squirrel_census.rs @@ -170,7 +170,7 @@ After writing the script, run it using python3 and show the results. Do not ask ExtensionRequirements { builtin: vec!["developer".to_string()], external: Vec::new(), - remote: Vec::new(), + streamable_http: Vec::new(), } } } diff --git a/crates/goose-cli/Cargo.toml b/crates/goose-cli/Cargo.toml index ee8bc7ba570f..738cc70f26bd 100644 --- a/crates/goose-cli/Cargo.toml +++ b/crates/goose-cli/Cargo.toml @@ -19,7 +19,7 @@ goose = { path = "../goose" } goose-bench = { path = "../goose-bench" } goose-mcp = { path = "../goose-mcp" } rmcp = { workspace = true } -sacp = "10.0.0-alpha.3" +sacp = { workspace = true } agent-client-protocol-schema = "0.10.5" clap = { version = "4.4", features = ["derive"] } cliclack = "0.3.5" diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 095a516bdb30..a00a54e5c40d 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -527,16 +527,6 @@ enum Command { )] extensions: Vec, - /// Add remote extensions with a URL - #[arg( - long = "with-remote-extension", - value_name = "URL", - help = "Add remote extensions (can be specified multiple times)", - long_help = "Add remote extensions from a URL. Can be specified multiple times. Format: 'url...'", - action = clap::ArgAction::Append - )] - remote_extensions: Vec, - /// Add streamable HTTP extensions with a URL #[arg( long = "with-streamable-http-extension", @@ -705,16 +695,6 @@ enum Command { )] extensions: Vec, - /// Add remote extensions - #[arg( - long = "with-remote-extension", - value_name = "URL", - help = "Add remote extensions (can be specified multiple times)", - long_help = "Add remote extensions. Can be specified multiple times. Format: 'url...'", - action = clap::ArgAction::Append - )] - remote_extensions: Vec, - /// Add streamable HTTP extensions #[arg( long = "with-streamable-http-extension", @@ -1017,7 +997,6 @@ pub async fn cli() -> anyhow::Result<()> { max_tool_repetitions, max_turns, extensions, - remote_extensions, streamable_http_extensions, builtins, }) => { @@ -1109,7 +1088,6 @@ pub async fn cli() -> anyhow::Result<()> { resume, no_session: false, extensions, - remote_extensions, streamable_http_extensions, builtins, extensions_override: None, @@ -1198,7 +1176,6 @@ pub async fn cli() -> anyhow::Result<()> { max_tool_repetitions, max_turns, extensions, - remote_extensions, streamable_http_extensions, builtins, params, @@ -1320,7 +1297,6 @@ pub async fn cli() -> anyhow::Result<()> { resume, no_session, extensions, - remote_extensions, streamable_http_extensions, builtins, extensions_override: input_config.extensions_override, @@ -1535,7 +1511,6 @@ pub async fn cli() -> anyhow::Result<()> { resume: false, no_session: false, extensions: Vec::new(), - remote_extensions: Vec::new(), streamable_http_extensions: Vec::new(), builtins: Vec::new(), extensions_override: None, diff --git a/crates/goose-cli/src/commands/acp.rs b/crates/goose-cli/src/commands/acp.rs index 89cbc2a15bd7..77d9308a21e6 100644 --- a/crates/goose-cli/src/commands/acp.rs +++ b/crates/goose-cli/src/commands/acp.rs @@ -13,14 +13,14 @@ use goose::session::SessionManager; use rmcp::model::{CallToolResult, RawContent, ResourceContents, Role}; use sacp::schema::{ AgentCapabilities, AuthenticateRequest, AuthenticateResponse, BlobResourceContents, - CancelNotification, ContentBlock, ContentChunk, EmbeddedResource, EmbeddedResourceResource, - ImageContent, InitializeRequest, InitializeResponse, LoadSessionRequest, LoadSessionResponse, - McpCapabilities, McpServer, NewSessionRequest, NewSessionResponse, PermissionOption, - PermissionOptionId, PermissionOptionKind, PromptCapabilities, PromptRequest, PromptResponse, - RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, SessionId, - SessionNotification, SessionUpdate, StopReason, TextContent, TextResourceContents, ToolCall, - ToolCallContent, ToolCallId, ToolCallLocation, ToolCallStatus, ToolCallUpdate, - ToolCallUpdateFields, ToolKind, + CancelNotification, Content, ContentBlock, ContentChunk, EmbeddedResource, + EmbeddedResourceResource, ImageContent, InitializeRequest, InitializeResponse, + LoadSessionRequest, LoadSessionResponse, McpCapabilities, McpServer, NewSessionRequest, + NewSessionResponse, PermissionOption, PermissionOptionId, PermissionOptionKind, + PromptCapabilities, PromptRequest, PromptResponse, RequestPermissionOutcome, + RequestPermissionRequest, ResourceLink, SessionId, SessionNotification, SessionUpdate, + StopReason, TextContent, TextResourceContents, ToolCall, ToolCallContent, ToolCallId, + ToolCallLocation, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, }; use sacp::{AgentToClient, ByteStreams, Handled, JrConnectionCx, JrMessageHandler, MessageCx}; use std::collections::{HashMap, HashSet}; @@ -46,49 +46,43 @@ struct GooseAcpAgent { fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result { match mcp_server { - McpServer::Stdio { - name, - command, - args, - env, - .. - } => Ok(ExtensionConfig::Stdio { - name, + McpServer::Stdio(stdio) => Ok(ExtensionConfig::Stdio { + name: stdio.name, description: String::new(), - cmd: command.to_string_lossy().to_string(), - args, - envs: Envs::new(env.into_iter().map(|e| (e.name, e.value)).collect()), + 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 { - name, url, headers, .. - } => Ok(ExtensionConfig::StreamableHttp { - name, + McpServer::Http(http) => Ok(ExtensionConfig::StreamableHttp { + name: http.name, description: String::new(), - uri: url, + uri: http.url, envs: Envs::default(), env_keys: vec![], - headers: headers.into_iter().map(|h| (h.name, h.value)).collect(), + headers: http + .headers + .into_iter() + .map(|h| (h.name, h.value)) + .collect(), timeout: None, bundled: Some(false), available_tools: vec![], }), - McpServer::Sse { name, .. } => Err(format!( - "SSE transport is deprecated and not supported: {}", - name - )), + McpServer::Sse(_) => Err("SSE is unsupported, migrate to streamable_http".to_string()), + _ => Err("Unknown MCP server type".to_string()), } } fn create_tool_location(path: &str, line: Option) -> ToolCallLocation { - ToolCallLocation { - path: path.into(), - line, - meta: None, + let mut loc = ToolCallLocation::new(path); + if let Some(l) = line { + loc = loc.line(l); } + loc } fn extract_tool_locations( @@ -393,6 +387,7 @@ impl GooseAcpAgent { } } ContentBlock::Audio(..) => (), + _ => (), // Handle any future ContentBlock variants } } @@ -409,18 +404,12 @@ impl GooseAcpAgent { match content_item { MessageContent::Text(text) => { // Stream text to the client - cx.send_notification(SessionNotification { - session_id: session_id.clone(), - update: SessionUpdate::AgentMessageChunk(ContentChunk { - content: ContentBlock::Text(TextContent { - text: text.text.clone(), - annotations: None, - meta: None, - }), - meta: None, - }), - meta: None, - })?; + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::AgentMessageChunk(ContentChunk::new(ContentBlock::Text( + TextContent::new(&text.text), + ))), + ))?; } MessageContent::ToolRequest(tool_request) => { self.handle_tool_request(tool_request, session_id, session, cx) @@ -432,18 +421,12 @@ impl GooseAcpAgent { } MessageContent::Thinking(thinking) => { // Stream thinking/reasoning content as thought chunks - cx.send_notification(SessionNotification { - session_id: session_id.clone(), - update: SessionUpdate::AgentThoughtChunk(ContentChunk { - content: ContentBlock::Text(TextContent { - text: thinking.thinking.clone(), - annotations: None, - meta: None, - }), - meta: None, - }), - meta: None, - })?; + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::AgentThoughtChunk(ContentChunk::new(ContentBlock::Text( + TextContent::new(&thinking.thinking), + ))), + ))?; } MessageContent::ActionRequired(action_required) => { if let ActionRequiredData::ToolConfirmation { @@ -489,21 +472,16 @@ impl GooseAcpAgent { }; // Send tool call notification using the provider's tool call ID directly - cx.send_notification(SessionNotification { - session_id: session_id.clone(), - update: SessionUpdate::ToolCall(ToolCall { - id: ToolCallId(tool_request.id.clone().into()), - title: format_tool_name(&tool_name), - kind: ToolKind::default(), - status: ToolCallStatus::Pending, - content: vec![], - locations: vec![], - raw_input: None, - raw_output: None, - meta: None, - }), - meta: None, - })?; + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::ToolCall( + ToolCall::new( + ToolCallId::new(tool_request.id.clone()), + format_tool_name(&tool_name), + ) + .status(ToolCallStatus::Pending), + ), + ))?; Ok(()) } @@ -532,27 +510,17 @@ impl GooseAcpAgent { }; // Send status update using provider's tool call ID directly - cx.send_notification(SessionNotification { - session_id: session_id.clone(), - update: SessionUpdate::ToolCallUpdate(ToolCallUpdate { - id: ToolCallId(tool_response.id.clone().into()), - fields: ToolCallUpdateFields { - status: Some(status), - content: Some(content), - locations: if locations.is_empty() { - None - } else { - Some(locations) - }, - title: None, - kind: None, - raw_input: None, - raw_output: None, - }, - meta: None, - }), - meta: None, - })?; + let mut fields = ToolCallUpdateFields::new().status(status).content(content); + if !locations.is_empty() { + fields = fields.locations(locations); + } + cx.send_notification(SessionNotification::new( + session_id.clone(), + SessionUpdate::ToolCallUpdate(ToolCallUpdate::new( + ToolCallId::new(tool_response.id.clone()), + fields, + )), + ))?; Ok(()) } @@ -573,27 +541,17 @@ impl GooseAcpAgent { let formatted_name = format_tool_name(&tool_name); // Use the request_id (provider's tool call ID) directly - let tool_call_update = ToolCallUpdate { - id: ToolCallId(request_id.clone().into()), - fields: ToolCallUpdateFields { - title: Some(formatted_name), - kind: Some(ToolKind::default()), - status: Some(ToolCallStatus::Pending), - content: prompt.map(|p| { - vec![ToolCallContent::Content { - content: ContentBlock::Text(TextContent { - text: p, - annotations: None, - meta: None, - }), - }] - }), - locations: None, - raw_input: Some(serde_json::Value::Object(arguments)), - raw_output: None, - }, - meta: None, - }; + let mut fields = ToolCallUpdateFields::new() + .title(formatted_name) + .kind(ToolKind::default()) + .status(ToolCallStatus::Pending) + .raw_input(serde_json::Value::Object(arguments)); + if let Some(p) = prompt { + fields = fields.content(vec![ToolCallContent::Content(Content::new( + ContentBlock::Text(TextContent::new(p)), + ))]); + } + let tool_call_update = ToolCallUpdate::new(ToolCallId::new(request_id.clone()), fields); fn option(kind: PermissionOptionKind) -> PermissionOption { let id = serde_json::to_value(kind) @@ -601,12 +559,7 @@ impl GooseAcpAgent { .as_str() .unwrap() .to_string(); - PermissionOption { - id: PermissionOptionId::from(id.clone()), - name: id, - kind, - meta: None, - } + PermissionOption::new(PermissionOptionId::from(id.clone()), id, kind) } let options = vec![ option(PermissionOptionKind::AllowAlways), @@ -614,12 +567,8 @@ impl GooseAcpAgent { option(PermissionOptionKind::RejectOnce), ]; - let permission_request = RequestPermissionRequest { - session_id, - tool_call: tool_call_update, - options, - meta: None, - }; + let permission_request = + RequestPermissionRequest::new(session_id, tool_call_update, options); cx.send_request(permission_request) .on_receiving_result(move |result| async move { @@ -656,18 +605,20 @@ impl GooseAcpAgent { fn outcome_to_confirmation(outcome: &RequestPermissionOutcome) -> PermissionConfirmation { let permission = match outcome { RequestPermissionOutcome::Cancelled => Permission::Cancel, - RequestPermissionOutcome::Selected { option_id } => { + RequestPermissionOutcome::Selected(selected) => { match serde_json::from_value::(serde_json::Value::String( - option_id.0.to_string(), + selected.option_id.to_string(), )) { Ok(PermissionOptionKind::AllowAlways) => Permission::AlwaysAllow, Ok(PermissionOptionKind::AllowOnce) => Permission::AllowOnce, Ok(PermissionOptionKind::RejectOnce | PermissionOptionKind::RejectAlways) => { Permission::DenyOnce } + Ok(_) => Permission::Cancel, // Handle any future permission kinds Err(_) => Permission::Cancel, } } + _ => Permission::Cancel, // Handle any future variants }; PermissionConfirmation { principal_type: PrincipalType::Tool, @@ -681,56 +632,43 @@ fn build_tool_call_content(tool_result: &ToolResult) -> Vec Some(ToolCallContent::Content { - content: ContentBlock::Text(TextContent { - text: val.text.clone(), - annotations: None, - meta: None, - }), - }), - RawContent::Image(val) => Some(ToolCallContent::Content { - content: ContentBlock::Image(ImageContent { - data: val.data.clone(), - mime_type: val.mime_type.clone(), - uri: None, - annotations: None, - meta: None, - }), - }), - RawContent::Resource(val) => Some(ToolCallContent::Content { - content: ContentBlock::Resource(EmbeddedResource { - resource: match &val.resource { - ResourceContents::TextResourceContents { - mime_type, - text, - uri, - .. - } => EmbeddedResourceResource::TextResourceContents( - TextResourceContents { - text: text.clone(), - uri: uri.clone(), - mime_type: mime_type.clone(), - meta: None, - }, - ), - ResourceContents::BlobResourceContents { - mime_type, - blob, - uri, - .. - } => EmbeddedResourceResource::BlobResourceContents( - BlobResourceContents { - blob: blob.clone(), - uri: uri.clone(), - mime_type: mime_type.clone(), - meta: None, - }, - ), - }, - annotations: None, - meta: None, - }), - }), + RawContent::Text(val) => Some(ToolCallContent::Content(Content::new( + ContentBlock::Text(TextContent::new(&val.text)), + ))), + RawContent::Image(val) => Some(ToolCallContent::Content(Content::new( + ContentBlock::Image(ImageContent::new(&val.data, &val.mime_type)), + ))), + RawContent::Resource(val) => { + let resource = match &val.resource { + ResourceContents::TextResourceContents { + mime_type, + text, + uri, + .. + } => { + let mut r = TextResourceContents::new(text.clone(), uri.clone()); + if let Some(mt) = mime_type { + r = r.mime_type(mt.clone()); + } + EmbeddedResourceResource::TextResourceContents(r) + } + ResourceContents::BlobResourceContents { + mime_type, + blob, + uri, + .. + } => { + let mut r = BlobResourceContents::new(blob.clone(), uri.clone()); + if let Some(mt) = mime_type { + r = r.mime_type(mt.clone()); + } + EmbeddedResourceResource::BlobResourceContents(r) + } + }; + Some(ToolCallContent::Content(Content::new( + ContentBlock::Resource(EmbeddedResource::new(resource)), + ))) + } RawContent::Audio(_) => { // Audio content is not supported in ACP ContentBlock, skip it None @@ -753,27 +691,16 @@ impl GooseAcpAgent { debug!(?args, "initialize request"); // Advertise Goose's capabilities - Ok(InitializeResponse { - protocol_version: args.protocol_version, - agent_capabilities: AgentCapabilities { - load_session: true, - prompt_capabilities: PromptCapabilities { - image: true, - audio: false, - embedded_context: true, - meta: None, - }, - mcp_capabilities: McpCapabilities { - http: true, - sse: false, // SSE is deprecated; rmcp drops support after 0.10.0 - meta: None, - }, - meta: None, - }, - auth_methods: vec![], - agent_info: None, - meta: None, - }) + let capabilities = AgentCapabilities::new() + .load_session(true) + .prompt_capabilities( + PromptCapabilities::new() + .image(true) + .audio(false) + .embedded_context(true), + ) + .mcp_capabilities(McpCapabilities::new().http(true)); + Ok(InitializeResponse::new(args.protocol_version).agent_capabilities(capabilities)) } async fn on_new_session( @@ -788,10 +715,11 @@ impl GooseAcpAgent { SessionType::User, ) .await - .map_err(|e| sacp::Error { - code: sacp::ErrorCode::INTERNAL_ERROR.code, - message: format!("Failed to create session: {}", e), - data: None, + .map_err(|e| { + sacp::Error::new( + sacp::ErrorCode::InternalError.into(), + format!("Failed to create session: {}", e), + ) })?; let session = GooseAcpSession { @@ -808,20 +736,15 @@ impl GooseAcpAgent { let config = match mcp_server_to_extension_config(mcp_server) { Ok(c) => c, Err(msg) => { - return Err(sacp::Error { - code: sacp::ErrorCode::INVALID_PARAMS.code, - message: msg, - data: None, - }); + return Err(sacp::Error::new(sacp::ErrorCode::InvalidParams.into(), msg)); } }; let name = config.name().to_string(); if let Err(e) = self.agent.add_extension(config).await { - return Err(sacp::Error { - code: sacp::ErrorCode::INTERNAL_ERROR.code, - message: format!("Failed to add MCP server '{}': {}", name, e), - data: None, - }); + return Err(sacp::Error::new( + sacp::ErrorCode::InternalError.into(), + format!("Failed to add MCP server '{}': {}", name, e), + )); } } @@ -831,11 +754,7 @@ impl GooseAcpAgent { "Session started" ); - Ok(NewSessionResponse { - session_id: SessionId(goose_session.id.into()), - modes: None, - meta: None, - }) + Ok(NewSessionResponse::new(SessionId::new(goose_session.id))) } async fn on_load_session( @@ -849,26 +768,29 @@ impl GooseAcpAgent { let goose_session = SessionManager::get_session(&session_id, true) .await - .map_err(|e| sacp::Error { - code: sacp::ErrorCode::INVALID_PARAMS.code, - message: format!("Failed to load session {}: {}", session_id, e), - data: None, + .map_err(|e| { + sacp::Error::new( + sacp::ErrorCode::InvalidParams.into(), + format!("Failed to load session {}: {}", session_id, e), + ) })?; - let conversation = goose_session.conversation.ok_or_else(|| sacp::Error { - code: sacp::ErrorCode::INTERNAL_ERROR.code, - message: format!("Session {} has no conversation data", session_id), - data: None, + let conversation = goose_session.conversation.ok_or_else(|| { + sacp::Error::new( + sacp::ErrorCode::InternalError.into(), + format!("Session {} has no conversation data", session_id), + ) })?; SessionManager::update_session(&session_id) .working_dir(args.cwd.clone()) .apply() .await - .map_err(|e| sacp::Error { - code: sacp::ErrorCode::INTERNAL_ERROR.code, - message: format!("Failed to update session working directory: {}", e), - data: None, + .map_err(|e| { + sacp::Error::new( + sacp::ErrorCode::InternalError.into(), + format!("Failed to update session working directory: {}", e), + ) })?; let mut session = GooseAcpSession { @@ -887,23 +809,16 @@ impl GooseAcpAgent { for content_item in &message.content { match content_item { MessageContent::Text(text) => { - let chunk = ContentChunk { - content: ContentBlock::Text(TextContent { - annotations: None, - text: text.text.clone(), - meta: None, - }), - meta: None, - }; + let chunk = + ContentChunk::new(ContentBlock::Text(TextContent::new(&text.text))); let update = match message.role { Role::User => SessionUpdate::UserMessageChunk(chunk), Role::Assistant => SessionUpdate::AgentMessageChunk(chunk), }; - cx.send_notification(SessionNotification { - session_id: args.session_id.clone(), + cx.send_notification(SessionNotification::new( + args.session_id.clone(), update, - meta: None, - })?; + ))?; } MessageContent::ToolRequest(tool_request) => { self.handle_tool_request(tool_request, &args.session_id, &mut session, cx) @@ -919,18 +834,12 @@ impl GooseAcpAgent { .await?; } MessageContent::Thinking(thinking) => { - cx.send_notification(SessionNotification { - session_id: args.session_id.clone(), - update: SessionUpdate::AgentThoughtChunk(ContentChunk { - content: ContentBlock::Text(TextContent { - annotations: None, - text: thinking.thinking.clone(), - meta: None, - }), - meta: None, - }), - meta: None, - })?; + cx.send_notification(SessionNotification::new( + args.session_id.clone(), + SessionUpdate::AgentThoughtChunk(ContentChunk::new( + ContentBlock::Text(TextContent::new(&thinking.thinking)), + )), + ))?; } _ => { // Ignore other content types @@ -948,10 +857,7 @@ impl GooseAcpAgent { "Session loaded" ); - Ok(LoadSessionResponse { - modes: None, - meta: None, - }) + Ok(LoadSessionResponse::new()) } async fn on_prompt( @@ -964,10 +870,11 @@ impl GooseAcpAgent { { let mut sessions = self.sessions.lock().await; - let session = sessions.get_mut(&session_id).ok_or_else(|| sacp::Error { - code: sacp::ErrorCode::INVALID_PARAMS.code, - message: format!("Session not found: {}", session_id), - data: None, + let session = sessions.get_mut(&session_id).ok_or_else(|| { + sacp::Error::new( + sacp::ErrorCode::InvalidParams.into(), + format!("Session not found: {}", session_id), + ) })?; session.cancel_token = Some(cancel_token.clone()); } @@ -985,10 +892,11 @@ impl GooseAcpAgent { .agent .reply(user_message, session_config, Some(cancel_token.clone())) .await - .map_err(|e| sacp::Error { - code: sacp::ErrorCode::INTERNAL_ERROR.code, - message: format!("Error getting agent reply: {}", e), - data: None, + .map_err(|e| { + sacp::Error::new( + sacp::ErrorCode::InternalError.into(), + format!("Error getting agent reply: {}", e), + ) })?; use futures::StreamExt; @@ -1004,10 +912,11 @@ impl GooseAcpAgent { match event { Ok(goose::agents::AgentEvent::Message(message)) => { let mut sessions = self.sessions.lock().await; - let session = sessions.get_mut(&session_id).ok_or_else(|| sacp::Error { - code: sacp::ErrorCode::INVALID_PARAMS.code, - message: format!("Session not found: {}", session_id), - data: None, + let session = sessions.get_mut(&session_id).ok_or_else(|| { + sacp::Error::new( + sacp::ErrorCode::InvalidParams.into(), + format!("Session not found: {}", session_id), + ) })?; session.messages.push(message.clone()); @@ -1019,11 +928,10 @@ impl GooseAcpAgent { } Ok(_) => {} Err(e) => { - return Err(sacp::Error { - code: sacp::ErrorCode::INTERNAL_ERROR.code, - message: format!("Error in agent response stream: {}", e), - data: None, - }); + return Err(sacp::Error::new( + sacp::ErrorCode::InternalError.into(), + format!("Error in agent response stream: {}", e), + )); } } } @@ -1033,14 +941,12 @@ impl GooseAcpAgent { session.cancel_token = None; } - Ok(PromptResponse { - stop_reason: if was_cancelled { - StopReason::Cancelled - } else { - StopReason::EndTurn - }, - meta: None, - }) + let stop_reason = if was_cancelled { + StopReason::Cancelled + } else { + StopReason::EndTurn + }; + Ok(PromptResponse::new(stop_reason)) } async fn on_cancel(&self, args: CancelNotification) -> Result<(), sacp::Error> { @@ -1090,7 +996,7 @@ impl JrMessageHandler for GooseAcpHandler { .await .if_request( |_req: AuthenticateRequest, req_cx: JrRequestCx| async { - req_cx.respond(AuthenticateResponse { meta: None }) + req_cx.respond(AuthenticateResponse::new()) }, ) .await @@ -1156,9 +1062,11 @@ pub async fn run_acp_agent(builtins: Vec) -> Result<()> { #[cfg(test)] mod tests { use super::*; - use sacp::schema::{EnvVariable, HttpHeader, McpServer, ResourceLink}; + use sacp::schema::{ + EnvVariable, HttpHeader, McpServer, McpServerHttp, McpServerSse, McpServerStdio, + ResourceLink, SelectedPermissionOutcome, + }; use std::io::Write; - use std::path::PathBuf; use tempfile::NamedTempFile; use test_case::test_case; @@ -1168,16 +1076,14 @@ mod tests { use goose::agents::ExtensionConfig; #[test_case( - McpServer::Stdio { - name: "github".into(), - command: PathBuf::from("/path/to/github-mcp-server"), - args: vec!["stdio".into()], - env: vec![EnvVariable { - name: "GITHUB_PERSONAL_ACCESS_TOKEN".into(), - value: "ghp_xxxxxxxxxxxx".into(), - meta: None, - }], - }, + McpServer::Stdio( + McpServerStdio::new("github", "/path/to/github-mcp-server") + .args(vec!["stdio".into()]) + .env(vec![EnvVariable::new( + "GITHUB_PERSONAL_ACCESS_TOKEN", + "ghp_xxxxxxxxxxxx" + )]) + ), Ok(ExtensionConfig::Stdio { name: "github".into(), description: String::new(), @@ -1197,15 +1103,10 @@ mod tests { }) )] #[test_case( - McpServer::Http { - name: "github".into(), - url: "https://api.githubcopilot.com/mcp/".into(), - headers: vec![HttpHeader { - name: "Authorization".into(), - value: "Bearer ghp_xxxxxxxxxxxx".into(), - meta: None, - }], - }, + McpServer::Http( + McpServerHttp::new("github", "https://api.githubcopilot.com/mcp/") + .headers(vec![HttpHeader::new("Authorization", "Bearer ghp_xxxxxxxxxxxx")]) + ), Ok(ExtensionConfig::StreamableHttp { name: "github".into(), description: String::new(), @@ -1222,12 +1123,8 @@ mod tests { }) )] #[test_case( - McpServer::Sse { - name: "test-sse".into(), - url: "https://example.com/sse".into(), - headers: vec![], - }, - Err("SSE transport is deprecated and not supported: test-sse".to_string()) + McpServer::Sse(McpServerSse::new("test-sse", "https://agent-fin.biodnd.com/sse")), + Err("SSE is unsupported, migrate to streamable_http".to_string()) )] fn test_mcp_server_to_extension_config( input: McpServer, @@ -1240,21 +1137,14 @@ mod tests { let mut file = NamedTempFile::new()?; file.write_all(content.as_bytes())?; - let link = ResourceLink { - name: file - .path() - .file_name() - .unwrap() - .to_string_lossy() - .to_string(), - uri: format!("file://{}", file.path().to_str().unwrap()), - annotations: None, - description: None, - mime_type: None, - size: None, - title: None, - meta: None, - }; + let name = file + .path() + .file_name() + .unwrap() + .to_string_lossy() + .to_string(); + let uri = format!("file://{}", file.path().to_str().unwrap()); + let link = ResourceLink::new(name, uri); Ok((link, file)) } @@ -1305,27 +1195,27 @@ print(\"hello, world\") } #[test_case( - RequestPermissionOutcome::Selected { option_id: PermissionOptionId::from("allow_once".to_string()) }, + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new("allow_once")), PermissionConfirmation { principal_type: PrincipalType::Tool, permission: Permission::AllowOnce }; "allow_once_maps_to_allow_once" )] #[test_case( - RequestPermissionOutcome::Selected { option_id: PermissionOptionId::from("allow_always".to_string()) }, + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new("allow_always")), PermissionConfirmation { principal_type: PrincipalType::Tool, permission: Permission::AlwaysAllow }; "allow_always_maps_to_always_allow" )] #[test_case( - RequestPermissionOutcome::Selected { option_id: PermissionOptionId::from("reject_once".to_string()) }, + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new("reject_once")), PermissionConfirmation { principal_type: PrincipalType::Tool, permission: Permission::DenyOnce }; "reject_once_maps_to_deny_once" )] #[test_case( - RequestPermissionOutcome::Selected { option_id: PermissionOptionId::from("reject_always".to_string()) }, + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new("reject_always")), PermissionConfirmation { principal_type: PrincipalType::Tool, permission: Permission::DenyOnce }; "reject_always_maps_to_deny_once" )] #[test_case( - RequestPermissionOutcome::Selected { option_id: PermissionOptionId::from("unknown".to_string()) }, + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new("unknown")), PermissionConfirmation { principal_type: PrincipalType::Tool, permission: Permission::Cancel }; "unknown_option_maps_to_cancel" )] diff --git a/crates/goose-cli/src/commands/bench.rs b/crates/goose-cli/src/commands/bench.rs index c0005fa5400c..10bd42539d29 100644 --- a/crates/goose-cli/src/commands/bench.rs +++ b/crates/goose-cli/src/commands/bench.rs @@ -38,8 +38,7 @@ pub async fn agent_generator( resume: false, no_session: false, extensions: requirements.external, - remote_extensions: requirements.remote, - streamable_http_extensions: Vec::new(), + streamable_http_extensions: requirements.streamable_http, builtins: requirements.builtin, extensions_override: None, additional_system_prompt: None, diff --git a/crates/goose-cli/src/commands/configure.rs b/crates/goose-cli/src/commands/configure.rs index 365e2e3f23d0..726087aefe75 100644 --- a/crates/goose-cli/src/commands/configure.rs +++ b/crates/goose-cli/src/commands/configure.rs @@ -625,6 +625,10 @@ pub async fn configure_provider_dialog() -> anyhow::Result { /// Configure extensions that can be used with goose /// Dialog for toggling which extensions are enabled/disabled pub fn toggle_extensions_dialog() -> anyhow::Result<()> { + for warning in goose::config::get_warnings() { + eprintln!("{}", style(format!("Warning: {}", warning)).yellow()); + } + let extensions = get_all_extensions(); if extensions.is_empty() { @@ -692,15 +696,10 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { "Command-line Extension", "Run a local command or script", ) - .item( - "sse", - "Remote Extension (SSE)", - "Connect to a remote extension via Server-Sent Events", - ) .item( "streamable_http", - "Remote Extension (Streaming HTTP)", - "Connect to a remote extension via MCP Streaming HTTP", + "Remote Extension (Streamable HTTP)", + "Connect to a remote extension via MCP Streamable HTTP", ) .interact()?; @@ -870,101 +869,6 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { cliclack::outro(format!("Added {} extension", style(name).green()))?; } - "sse" => { - let extensions = get_all_extension_names(); - let name: String = cliclack::input("What would you like to call this extension?") - .placeholder("my-remote-extension") - .validate(move |input: &String| { - if input.is_empty() { - Err("Please enter a name") - } else if extensions.contains(input) { - Err("An extension with this name already exists") - } else { - Ok(()) - } - }) - .interact()?; - - let uri: String = cliclack::input("What is the SSE endpoint URI?") - .placeholder("http://localhost:8000/events") - .validate(|input: &String| { - if input.is_empty() { - Err("Please enter a URI") - } else if !input.starts_with("http") { - Err("URI should start with http:// or https://") - } else { - Ok(()) - } - }) - .interact()?; - - let timeout: u64 = cliclack::input("Please set the timeout for this tool (in secs):") - .placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string()) - .validate(|input: &String| match input.parse::() { - Ok(_) => Ok(()), - Err(_) => Err("Please enter a valid timeout"), - }) - .interact()?; - - let description = cliclack::input("Enter a description for this extension:") - .placeholder("Description") - .validate(|input: &String| match input.parse::() { - Ok(_) => Ok(()), - Err(_) => Err("Please enter a valid description"), - }) - .interact()?; - let add_env = - cliclack::confirm("Would you like to add environment variables?").interact()?; - - let mut envs = HashMap::new(); - let mut env_keys = Vec::new(); - let config = Config::global(); - - if add_env { - loop { - let key: String = cliclack::input("Environment variable name:") - .placeholder("API_KEY") - .interact()?; - - let value: String = cliclack::password("Environment variable value:") - .mask('▪') - .interact()?; - - // Try to store in keychain - let keychain_key = key.to_string(); - match config.set_secret(&keychain_key, &value) { - Ok(_) => { - // Successfully stored in keychain, add to env_keys - env_keys.push(keychain_key); - } - Err(_) => { - // Failed to store in keychain, store directly in envs - envs.insert(key, value); - } - } - - if !cliclack::confirm("Add another environment variable?").interact()? { - break; - } - } - } - - set_extension(ExtensionEntry { - enabled: true, - config: ExtensionConfig::Sse { - name: name.clone(), - uri, - envs: Envs::new(envs), - env_keys, - description, - timeout: Some(timeout), - bundled: None, - available_tools: Vec::new(), - }, - }); - - cliclack::outro(format!("Added {} extension", style(name).green()))?; - } "streamable_http" => { let extensions = get_all_extension_names(); let name: String = cliclack::input("What would you like to call this extension?") @@ -980,7 +884,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { }) .interact()?; - let uri: String = cliclack::input("What is the Streaming HTTP endpoint URI?") + let uri: String = cliclack::input("What is the Streamable HTTP endpoint URI?") .placeholder("http://localhost:8000/messages") .validate(|input: &String| { if input.is_empty() { @@ -1034,7 +938,7 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { } } - let add_env = false; // No env prompt for Streaming HTTP + let add_env = false; // No env prompt for Streamable HTTP let mut envs = HashMap::new(); let mut env_keys = Vec::new(); @@ -1095,6 +999,10 @@ pub fn configure_extensions_dialog() -> anyhow::Result<()> { } pub fn remove_extension_dialog() -> anyhow::Result<()> { + for warning in goose::config::get_warnings() { + eprintln!("{}", style(format!("Warning: {}", warning)).yellow()); + } + let extensions = get_all_extensions(); // Create a list of extension names and their enabled status diff --git a/crates/goose-cli/src/recipes/secret_discovery.rs b/crates/goose-cli/src/recipes/secret_discovery.rs index 8820ec409cf1..82dfb0177e4c 100644 --- a/crates/goose-cli/src/recipes/secret_discovery.rs +++ b/crates/goose-cli/src/recipes/secret_discovery.rs @@ -51,13 +51,17 @@ fn extract_secrets_from_extensions( for ext in extensions { let (extension_name, env_keys) = match ext { - ExtensionConfig::Sse { name, env_keys, .. } => (name, env_keys), ExtensionConfig::Stdio { name, env_keys, .. } => (name, env_keys), ExtensionConfig::StreamableHttp { name, env_keys, .. } => (name, env_keys), ExtensionConfig::Builtin { name, .. } => (name, &Vec::new()), ExtensionConfig::Platform { name, .. } => (name, &Vec::new()), ExtensionConfig::Frontend { name, .. } => (name, &Vec::new()), ExtensionConfig::InlinePython { name, .. } => (name, &Vec::new()), + // SSE is unsupported - skip + ExtensionConfig::Sse { name, .. } => { + tracing::warn!(name = %name, "SSE is unsupported, skipping"); + continue; + } }; for key in env_keys { @@ -136,15 +140,16 @@ mod tests { instructions: Some("Test instructions".to_string()), prompt: None, extensions: Some(vec![ - ExtensionConfig::Sse { + ExtensionConfig::StreamableHttp { name: "github-mcp".to_string(), - uri: "sse://example.com".to_string(), + uri: "http://localhost:8080/mcp".to_string(), envs: Envs::new(HashMap::new()), env_keys: vec!["GITHUB_TOKEN".to_string(), "GITHUB_API_URL".to_string()], description: "github-mcp".to_string(), timeout: None, bundled: None, available_tools: Vec::new(), + headers: HashMap::new(), }, ExtensionConfig::Stdio { name: "slack-mcp".to_string(), @@ -231,15 +236,16 @@ mod tests { instructions: Some("Test instructions".to_string()), prompt: None, extensions: Some(vec![ - ExtensionConfig::Sse { + ExtensionConfig::StreamableHttp { name: "service-a".to_string(), - uri: "sse://example.com".to_string(), + uri: "http://localhost:8080/mcp".to_string(), envs: Envs::new(HashMap::new()), env_keys: vec!["API_KEY".to_string()], description: "service-a".to_string(), timeout: None, bundled: None, available_tools: Vec::new(), + headers: HashMap::new(), }, ExtensionConfig::Stdio { name: "service-b".to_string(), @@ -289,15 +295,16 @@ mod tests { description: "A recipe with sub-recipes".to_string(), instructions: Some("Test instructions".to_string()), prompt: None, - extensions: Some(vec![ExtensionConfig::Sse { + extensions: Some(vec![ExtensionConfig::StreamableHttp { name: "parent-ext".to_string(), - uri: "sse://parent.com".to_string(), + uri: "http://localhost:8080/mcp".to_string(), envs: Envs::new(HashMap::new()), env_keys: vec!["PARENT_TOKEN".to_string()], description: "parent-ext".to_string(), timeout: None, bundled: None, available_tools: Vec::new(), + headers: HashMap::new(), }]), sub_recipes: Some(vec![SubRecipe { name: "child-recipe".to_string(), diff --git a/crates/goose-cli/src/scenario_tests/mock_client.rs b/crates/goose-cli/src/scenario_tests/mock_client.rs index a042c55a93bc..1314f251e76a 100644 --- a/crates/goose-cli/src/scenario_tests/mock_client.rs +++ b/crates/goose-cli/src/scenario_tests/mock_client.rs @@ -50,6 +50,7 @@ impl McpClientTrait for MockClient { Ok(ListResourcesResult { resources: vec![], next_cursor: None, + meta: None, }) } @@ -85,6 +86,7 @@ impl McpClientTrait for MockClient { Ok(ListToolsResult { tools: rmcp_tools, next_cursor: None, + meta: None, }) } @@ -117,6 +119,7 @@ impl McpClientTrait for MockClient { Ok(ListPromptsResult { prompts: vec![], next_cursor: None, + meta: None, }) } diff --git a/crates/goose-cli/src/session/builder.rs b/crates/goose-cli/src/session/builder.rs index 049f48e7a24f..11e15f7717b0 100644 --- a/crates/goose-cli/src/session/builder.rs +++ b/crates/goose-cli/src/session/builder.rs @@ -34,8 +34,6 @@ pub struct SessionBuilderConfig { pub no_session: bool, /// List of stdio extension commands to add pub extensions: Vec, - /// List of remote extension commands to add - pub remote_extensions: Vec, /// List of streamable HTTP extension commands to add pub streamable_http_extensions: Vec, /// List of builtin extension commands to add @@ -81,7 +79,6 @@ impl Default for SessionBuilderConfig { resume: false, no_session: false, extensions: Vec::new(), - remote_extensions: Vec::new(), streamable_http_extensions: Vec::new(), builtins: Vec::new(), extensions_override: None, @@ -422,6 +419,11 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { // Setup extensions for the agent // Extensions need to be added after the session is created because we change directory when resuming a session + + for warning in goose::config::get_warnings() { + eprintln!("{}", style(format!("Warning: {}", warning)).yellow()); + } + // If we get extensions_override, only run those extensions and none other let extensions_to_run: Vec<_> = if let Some(extensions) = session_config.extensions_override { extensions.into_iter().collect() @@ -548,32 +550,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession { } } - // Add remote extensions if provided - for extension_str in session_config.remote_extensions { - if let Err(e) = session.add_remote_extension(extension_str.clone()).await { - eprintln!( - "{}", - style(format!( - "Warning: Failed to start remote extension '{}' ({}), continuing without it", - extension_str, e - )) - .yellow() - ); - - // Offer debugging help - if let Err(debug_err) = offer_extension_debugging_help( - &extension_str, - &e.to_string(), - Arc::clone(&provider_for_display), - session_config.interactive, - ) - .await - { - eprintln!("Note: Could not start debugging session: {}", debug_err); - } - } - } - // Add streamable HTTP extensions if provided for extension_str in session_config.streamable_http_extensions { if let Err(e) = session @@ -686,8 +662,7 @@ mod tests { resume: false, no_session: false, extensions: vec!["echo test".to_string()], - remote_extensions: vec!["http://example.com".to_string()], - streamable_http_extensions: vec!["http://example.com/streamable".to_string()], + streamable_http_extensions: vec!["http://localhost:8080/mcp".to_string()], builtins: vec!["developer".to_string()], extensions_override: None, additional_system_prompt: Some("Test prompt".to_string()), @@ -707,7 +682,6 @@ mod tests { }; assert_eq!(config.extensions.len(), 1); - assert_eq!(config.remote_extensions.len(), 1); assert_eq!(config.streamable_http_extensions.len(), 1); assert_eq!(config.builtins.len(), 1); assert!(config.debug); @@ -726,7 +700,6 @@ mod tests { assert!(!config.resume); assert!(!config.no_session); assert!(config.extensions.is_empty()); - assert!(config.remote_extensions.is_empty()); assert!(config.streamable_http_extensions.is_empty()); assert!(config.builtins.is_empty()); assert!(config.extensions_override.is_none()); diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 356aeed04673..ded355467c74 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -254,34 +254,6 @@ impl CliSession { Ok(()) } - /// Add a remote extension to the session - /// - /// # Arguments - /// * `extension_url` - URL of the server - pub async fn add_remote_extension(&mut self, extension_url: String) -> Result<()> { - let config = ExtensionConfig::Sse { - name: String::new(), - uri: extension_url, - envs: Envs::new(HashMap::new()), - env_keys: Vec::new(), - description: goose::config::DEFAULT_EXTENSION_DESCRIPTION.to_string(), - // TODO: should set timeout - timeout: Some(goose::config::DEFAULT_EXTENSION_TIMEOUT), - bundled: None, - available_tools: Vec::new(), - }; - - self.agent - .add_extension(config) - .await - .map_err(|e| anyhow::anyhow!("Failed to start extension: {}", e))?; - - // Invalidate the completion cache when a new extension is added - self.invalidate_completion_cache().await; - - Ok(()) - } - /// Add a streamable HTTP extension to the session /// /// # Arguments diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index 3d125f3f2a03..5b632aebc169 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -1330,6 +1330,7 @@ impl ServerHandler for ComputerControllerServer { Ok(ListResourcesResult { resources, next_cursor: None, + meta: None, }) } diff --git a/crates/goose-mcp/src/developer/rmcp_developer.rs b/crates/goose-mcp/src/developer/rmcp_developer.rs index 8f5c417373c4..eb788086fe42 100644 --- a/crates/goose-mcp/src/developer/rmcp_developer.rs +++ b/crates/goose-mcp/src/developer/rmcp_developer.rs @@ -400,6 +400,7 @@ impl ServerHandler for DeveloperServer { std::future::ready(Ok(ListPromptsResult { prompts, next_cursor: None, + meta: None, })) } diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index 9c483f4af855..83f94aba37de 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -30,6 +30,8 @@ use utoipa::ToSchema; #[derive(Serialize, ToSchema)] pub struct ExtensionResponse { pub extensions: Vec, + #[serde(default)] + pub warnings: Vec, } #[derive(Deserialize, ToSchema)] @@ -254,7 +256,11 @@ pub async fn read_config( )] pub async fn get_extensions() -> Result, StatusCode> { let extensions = goose::config::get_all_extensions(); - Ok(Json(ExtensionResponse { extensions })) + let warnings = goose::config::get_warnings(); + Ok(Json(ExtensionResponse { + extensions, + warnings, + })) } #[utoipa::path( diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index 3a3f3c495a88..9a2f93eb81e3 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -20,8 +20,6 @@ rmcp = { workspace = true, features = [ "client", "reqwest", "transport-child-process", - "transport-sse-client", - "transport-sse-client-reqwest", "transport-streamable-http-client", "transport-streamable-http-client-reqwest", ] } @@ -121,7 +119,7 @@ unbinder = "0.1.7" winapi = { version = "0.3", features = ["wincred"] } [dev-dependencies] -sacp = "10.0.0-alpha.3" +sacp = { workspace = true } agent-client-protocol-schema = "0.10.5" criterion = "0.5" serial_test = "3.2.0" diff --git a/crates/goose/src/agents/chatrecall_extension.rs b/crates/goose/src/agents/chatrecall_extension.rs index 6176d4d42ef0..5d45976c8077 100644 --- a/crates/goose/src/agents/chatrecall_extension.rs +++ b/crates/goose/src/agents/chatrecall_extension.rs @@ -302,6 +302,7 @@ impl McpClientTrait for ChatRecallClient { Ok(ListToolsResult { tools: Self::get_tools(), next_cursor: None, + meta: None, }) } diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/code_execution_extension.rs index b041addc4cd7..82a07853d96c 100644 --- a/crates/goose/src/agents/code_execution_extension.rs +++ b/crates/goose/src/agents/code_execution_extension.rs @@ -835,6 +835,7 @@ impl McpClientTrait for CodeExecutionClient { }), ], next_cursor: None, + meta: None, }) } diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 22b13adb7327..ef18d76ca50e 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -233,27 +233,18 @@ impl Envs { #[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq)] #[serde(tag = "type")] pub enum ExtensionConfig { - /// Server-sent events client with a URI endpoint + /// SSE transport is no longer supported - kept only for config file compatibility #[serde(rename = "sse")] Sse { - /// The name used to identify this extension + #[serde(default)] + #[schema(required)] name: String, #[serde(default)] #[serde(deserialize_with = "deserialize_null_with_default")] #[schema(required)] description: String, - uri: String, - #[serde(default)] - envs: Envs, - #[serde(default)] - env_keys: Vec, - // NOTE: set timeout to be optional for compatibility. - // However, new configurations should include this field. - timeout: Option, #[serde(default)] - bundled: Option, - #[serde(default)] - available_tools: Vec, + uri: Option, }, /// Standard I/O client with command and arguments #[serde(rename = "stdio")] @@ -379,19 +370,6 @@ impl Default for ExtensionConfig { } impl ExtensionConfig { - pub fn sse, T: Into>(name: S, uri: S, description: S, timeout: T) -> Self { - Self::Sse { - name: name.into(), - uri: uri.into(), - envs: Envs::default(), - env_keys: Vec::new(), - description: description.into(), - timeout: Some(timeout.into()), - bundled: None, - available_tools: Vec::new(), - } - } - pub fn streamable_http, T: Into>( name: S, uri: S, @@ -499,10 +477,8 @@ impl ExtensionConfig { /// Check if a tool should be available to the LLM pub fn is_tool_available(&self, tool_name: &str) -> bool { let available_tools = match self { - Self::Sse { - available_tools, .. - } - | Self::StreamableHttp { + Self::Sse { .. } => return false, // SSE is unsupported + Self::StreamableHttp { available_tools, .. } | Self::Stdio { @@ -531,7 +507,9 @@ impl ExtensionConfig { 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::Sse { name, .. } => { + write!(f, "SSE({}: unsupported)", name) + } ExtensionConfig::StreamableHttp { name, uri, .. } => { write!(f, "StreamableHttp({}: {})", name, uri) } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index b9f8e6123dfc..ceaf7d11c634 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -9,8 +9,7 @@ use rmcp::transport::streamable_http_client::{ AuthRequiredError, StreamableHttpClientTransportConfig, StreamableHttpError, }; use rmcp::transport::{ - ConfigureCommandExt, DynamicTransportError, SseClientTransport, StreamableHttpClientTransport, - TokioChildProcess, + ConfigureCommandExt, DynamicTransportError, StreamableHttpClientTransport, TokioChildProcess, }; use std::collections::HashMap; use std::option::Option; @@ -379,25 +378,10 @@ impl ExtensionManager { } let client: Box = match &config { - ExtensionConfig::Sse { uri, timeout, .. } => { - let transport = SseClientTransport::start(uri.to_string()).await.map_err( - |transport_error| { - ClientInitializeError::transport::>( - transport_error, - "connect", - ) - }, - )?; - Box::new( - McpClient::connect( - transport, - Duration::from_secs( - timeout.unwrap_or(crate::config::DEFAULT_EXTENSION_TIMEOUT), - ), - self.provider.clone(), - ) - .await?, - ) + ExtensionConfig::Sse { .. } => { + return Err(ExtensionError::ConfigError( + "SSE is unsupported, migrate to streamable_http".to_string(), + )); } ExtensionConfig::StreamableHttp { uri, @@ -1250,8 +1234,8 @@ impl ExtensionManager { description } } + ExtensionConfig::Sse { .. } => "SSE extension (unsupported)", ExtensionConfig::Platform { description, .. } - | ExtensionConfig::Sse { description, .. } | ExtensionConfig::StreamableHttp { description, .. } | ExtensionConfig::Stdio { description, .. } | ExtensionConfig::Frontend { description, .. } @@ -1426,6 +1410,7 @@ mod tests { ), ], next_cursor: None, + meta: None, }) } diff --git a/crates/goose/src/agents/extension_manager_extension.rs b/crates/goose/src/agents/extension_manager_extension.rs index 8f751af6d1e5..c8304fbb25a3 100644 --- a/crates/goose/src/agents/extension_manager_extension.rs +++ b/crates/goose/src/agents/extension_manager_extension.rs @@ -410,6 +410,7 @@ impl McpClientTrait for ExtensionManagerClient { Ok(ListToolsResult { tools: self.get_tools().await, next_cursor: None, + meta: None, }) } diff --git a/crates/goose/src/agents/skills_extension.rs b/crates/goose/src/agents/skills_extension.rs index cd90fe6b2687..24e50fca61a8 100644 --- a/crates/goose/src/agents/skills_extension.rs +++ b/crates/goose/src/agents/skills_extension.rs @@ -292,6 +292,7 @@ impl McpClientTrait for SkillsClient { Ok(ListToolsResult { tools, next_cursor: None, + meta: None, }) } diff --git a/crates/goose/src/agents/todo_extension.rs b/crates/goose/src/agents/todo_extension.rs index 1ab8c818fd7b..4b1c0b686176 100644 --- a/crates/goose/src/agents/todo_extension.rs +++ b/crates/goose/src/agents/todo_extension.rs @@ -193,6 +193,7 @@ impl McpClientTrait for TodoClient { Ok(ListToolsResult { tools: Self::get_tools(), next_cursor: None, + meta: None, }) } diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index a7bf586c8dec..10e9d5dc67bc 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -41,8 +41,8 @@ fn get_extensions_map() -> IndexMap { let mut extensions_map = IndexMap::with_capacity(raw.len()); for (k, v) in raw { match (k, serde_yaml::from_value::(v)) { - (serde_yaml::Value::String(s), Ok(entry)) => { - extensions_map.insert(s, entry); + (serde_yaml::Value::String(key), Ok(entry)) => { + extensions_map.insert(key, entry); } (k, v) => { warn!( @@ -134,3 +134,24 @@ pub fn get_enabled_extensions() -> Vec { .map(|ext| ext.config) .collect() } + +pub fn get_warnings() -> Vec { + let raw: Mapping = Config::global() + .get_param(EXTENSIONS_CONFIG_KEY) + .unwrap_or_default(); + + let mut warnings = Vec::new(); + for (k, v) in raw { + if let (serde_yaml::Value::String(key), Ok(entry)) = + (k, serde_yaml::from_value::(v)) + { + if matches!(entry.config, ExtensionConfig::Sse { .. }) { + warnings.push(format!( + "'{}': SSE is unsupported, migrate to streamable_http", + key + )); + } + } + } + warnings +} diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index ffae26d460aa..f7e3b8ad31b1 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -15,7 +15,8 @@ pub use declarative_providers::DeclarativeProviderConfig; pub use experiments::ExperimentManager; pub use extensions::{ get_all_extension_names, get_all_extensions, get_enabled_extensions, get_extension_by_name, - is_extension_enabled, remove_extension, set_extension, set_extension_enabled, ExtensionEntry, + get_warnings, is_extension_enabled, remove_extension, set_extension, set_extension_enabled, + ExtensionEntry, }; pub use goose_mode::GooseMode; pub use permission::PermissionManager; diff --git a/crates/goose/src/recipe/recipe_extension_adapter.rs b/crates/goose/src/recipe/recipe_extension_adapter.rs index c9c538ca6793..32e680a69928 100644 --- a/crates/goose/src/recipe/recipe_extension_adapter.rs +++ b/crates/goose/src/recipe/recipe_extension_adapter.rs @@ -7,22 +7,6 @@ use std::collections::HashMap; #[derive(Deserialize)] #[serde(tag = "type")] enum RecipeExtensionConfigInternal { - #[serde(rename = "sse")] - Sse { - name: String, - #[serde(default)] - description: Option, - uri: String, - #[serde(default)] - envs: Envs, - #[serde(default)] - env_keys: Vec, - timeout: Option, - #[serde(default)] - bundled: Option, - #[serde(default)] - available_tools: Vec, - }, #[serde(rename = "stdio")] Stdio { name: String, @@ -127,15 +111,7 @@ macro_rules! map_recipe_extensions { impl From for ExtensionConfig { fn from(internal_variant: RecipeExtensionConfigInternal) -> Self { map_recipe_extensions!( - internal_variant; - Sse { - uri, - envs, - env_keys, - timeout, - bundled, - available_tools - }, + internal_variant; Stdio { cmd, args, @@ -187,12 +163,7 @@ where D: Deserializer<'de>, { let remotes = Option::>::deserialize(deserializer)?; - Ok(remotes.map(|items| { - items - .into_iter() - .map(ExtensionConfig::from) - .collect::>() - })) + Ok(remotes.map(|items| items.into_iter().map(ExtensionConfig::from).collect())) } #[cfg(test)] diff --git a/crates/goose/tests/acp_integration_test.rs b/crates/goose/tests/acp_integration_test.rs index bc23e9683f4f..290fc309f92d 100644 --- a/crates/goose/tests/acp_integration_test.rs +++ b/crates/goose/tests/acp_integration_test.rs @@ -8,9 +8,10 @@ use rmcp::{ ErrorData as McpError, ServerHandler, }; use sacp::schema::{ - ContentBlock, ContentChunk, InitializeRequest, McpServer, NewSessionRequest, PromptRequest, - RequestPermissionOutcome, RequestPermissionRequest, RequestPermissionResponse, - SessionNotification, SessionUpdate, StopReason, TextContent, VERSION as PROTOCOL_VERSION, + ContentBlock, ContentChunk, InitializeRequest, McpServer, McpServerHttp, NewSessionRequest, + PromptRequest, ProtocolVersion, RequestPermissionOutcome, RequestPermissionRequest, + RequestPermissionResponse, SelectedPermissionOutcome, SessionNotification, SessionUpdate, + StopReason, TextContent, }; use sacp::{ClientToAgent, JrConnectionCx}; use std::collections::VecDeque; @@ -43,15 +44,10 @@ async fn test_acp_basic_completion() { tempfile::tempdir().unwrap().path(), |cx, session_id, updates| async move { let response = cx - .send_request(PromptRequest { + .send_request(PromptRequest::new( session_id, - prompt: vec![ContentBlock::Text(TextContent { - text: prompt.to_string(), - annotations: None, - meta: None, - })], - meta: None, - }) + vec![ContentBlock::Text(TextContent::new(prompt))], + )) .block_task() .await .unwrap(); @@ -82,24 +78,15 @@ async fn test_acp_with_mcp_http_server() { run_acp_session( &mock_server, - vec![McpServer::Http { - name: "lookup".into(), - url: mcp_url, - headers: vec![], - }], + vec![McpServer::Http(McpServerHttp::new("lookup", &mcp_url))], &[], tempfile::tempdir().unwrap().path(), |cx, session_id, updates| async move { let response = cx - .send_request(PromptRequest { + .send_request(PromptRequest::new( session_id, - prompt: vec![ContentBlock::Text(TextContent { - text: prompt.to_string(), - annotations: None, - meta: None, - })], - meta: None, - }) + vec![ContentBlock::Text(TextContent::new(prompt))], + )) .block_task() .await .unwrap(); @@ -139,24 +126,15 @@ async fn test_acp_with_builtin_and_mcp() { run_acp_session( &mock_server, - vec![McpServer::Http { - name: "lookup".into(), - url: mcp_url, - headers: vec![], - }], + vec![McpServer::Http(McpServerHttp::new("lookup", &mcp_url))], &["code_execution", "developer"], tempfile::tempdir().unwrap().path(), |cx, session_id, updates| async move { let response = cx - .send_request(PromptRequest { + .send_request(PromptRequest::new( session_id, - prompt: vec![ContentBlock::Text(TextContent { - text: prompt.to_string(), - annotations: None, - meta: None, - })], - meta: None, - }) + vec![ContentBlock::Text(TextContent::new(prompt))], + )) .block_task() .await .unwrap(); @@ -309,16 +287,14 @@ async fn run_acp_session( ) .on_receive_request( async move |request: RequestPermissionRequest, request_cx, _connection_cx| { - let option_id = request.options.first().map(|opt| opt.id.clone()); + let option_id = request.options.first().map(|opt| opt.option_id.clone()); match option_id { - Some(id) => request_cx.respond(RequestPermissionResponse { - outcome: RequestPermissionOutcome::Selected { option_id: id }, - meta: None, - }), - None => request_cx.respond(RequestPermissionResponse { - outcome: RequestPermissionOutcome::Cancelled, - meta: None, - }), + Some(id) => request_cx.respond(RequestPermissionResponse::new( + RequestPermissionOutcome::Selected(SelectedPermissionOutcome::new(id)), + )), + None => request_cx.respond(RequestPermissionResponse::new( + RequestPermissionOutcome::Cancelled, + )), } }, sacp::on_receive_request!(), @@ -328,22 +304,16 @@ async fn run_acp_session( .run_until({ let updates = updates.clone(); move |cx: JrConnectionCx| async move { - cx.send_request(InitializeRequest { - protocol_version: PROTOCOL_VERSION, - client_capabilities: Default::default(), - client_info: Default::default(), - meta: None, - }) - .block_task() - .await - .unwrap(); + cx.send_request(InitializeRequest::new(ProtocolVersion::LATEST)) + .block_task() + .await + .unwrap(); let session = cx - .send_request(NewSessionRequest { - mcp_servers, - cwd: work_dir.path().to_path_buf(), - meta: None, - }) + .send_request( + NewSessionRequest::new(work_dir.path().to_path_buf()) + .mcp_servers(mcp_servers), + ) .block_task() .await .unwrap(); @@ -380,7 +350,7 @@ impl Lookup { impl ServerHandler for Lookup { fn get_info(&self) -> ServerInfo { ServerInfo { - protocol_version: ProtocolVersion::V_2025_03_26, + protocol_version: rmcp::model::ProtocolVersion::V_2025_03_26, capabilities: ServerCapabilities::builder().enable_tools().build(), server_info: Implementation { name: "lookup".into(), diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 517e9aae9c8c..0094986cda49 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -3147,45 +3147,18 @@ "oneOf": [ { "type": "object", - "description": "Server-sent events client with a URI endpoint", + "description": "SSE transport is no longer supported - kept only for config file compatibility", "required": [ "name", "description", - "uri", "type" ], "properties": { - "available_tools": { - "type": "array", - "items": { - "type": "string" - } - }, - "bundled": { - "type": "boolean", - "nullable": true - }, "description": { "type": "string" }, - "env_keys": { - "type": "array", - "items": { - "type": "string" - } - }, - "envs": { - "$ref": "#/components/schemas/Envs" - }, "name": { - "type": "string", - "description": "The name used to identify this extension" - }, - "timeout": { - "type": "integer", - "format": "int64", - "nullable": true, - "minimum": 0 + "type": "string" }, "type": { "type": "string", @@ -3194,7 +3167,8 @@ ] }, "uri": { - "type": "string" + "type": "string", + "nullable": true } } }, @@ -3551,6 +3525,12 @@ "items": { "$ref": "#/components/schemas/ExtensionEntry" } + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } } } }, @@ -4523,6 +4503,10 @@ "name" ], "properties": { + "_meta": { + "type": "object", + "additionalProperties": true + }, "description": { "type": "string" }, diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json index ef2156aa3de1..72993ddaab6e 100644 --- a/ui/desktop/package-lock.json +++ b/ui/desktop/package-lock.json @@ -325,7 +325,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -695,7 +694,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -739,7 +737,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1179,7 +1176,6 @@ "integrity": "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", @@ -2728,7 +2724,6 @@ "integrity": "sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^3.0.1", "@inquirer/confirm": "^4.0.1", @@ -6133,7 +6128,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -6421,7 +6417,6 @@ "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -6463,7 +6458,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -6474,7 +6468,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -6609,7 +6602,6 @@ "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.48.1", "@typescript-eslint/types": "8.48.1", @@ -7005,7 +6997,6 @@ "integrity": "sha512-sxSyJMaKp45zI0u+lHrPuZM1ZJQ8FaVD35k+UxVrha1yyvQ+TZuUYllUixwvQXlB7ixoDc7skf3lQPopZIvaQw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "4.0.15", "fflate": "^0.8.2", @@ -7254,7 +7245,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7899,7 +7889,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -9187,7 +9176,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -9245,7 +9235,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", @@ -10260,7 +10249,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -10720,7 +10708,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -13017,7 +13004,6 @@ "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.23", "@asamuzakjp/dom-selector": "^6.7.4", @@ -14261,6 +14247,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -14281,7 +14268,6 @@ "integrity": "sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", @@ -16661,7 +16647,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16763,6 +16748,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -16778,6 +16764,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -17133,7 +17120,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -17143,7 +17129,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -17165,7 +17150,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-markdown": { "version": "10.1.0", @@ -18146,7 +18132,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19073,8 +19058,7 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -19604,7 +19588,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -20019,7 +20002,6 @@ "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -20110,7 +20092,6 @@ "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/expect": "4.0.15", "@vitest/mocker": "4.0.15", @@ -20796,7 +20777,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index 8a4d22bd044a..fb2745b028db 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -230,18 +230,10 @@ export type ErrorResponse = { * Represents the different types of MCP extensions that can be added to the manager */ export type ExtensionConfig = { - available_tools?: Array; - bundled?: boolean | null; description: string; - env_keys?: Array; - envs?: Envs; - /** - * The name used to identify this extension - */ name: string; - timeout?: number | null; type: 'sse'; - uri: string; + uri?: string | null; } | { args: Array; available_tools?: Array; @@ -351,6 +343,7 @@ export type ExtensionQuery = { export type ExtensionResponse = { extensions: Array; + warnings?: Array; }; export type FrontendToolRequest = { @@ -667,6 +660,9 @@ export type RawImageContent = { }; export type RawResource = { + _meta?: { + [key: string]: unknown; + }; description?: string; icons?: Array; mimeType?: string; diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index 5f4f16c827c0..cee6a79b7c13 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -31,6 +31,7 @@ interface ConfigContextType { config: ConfigResponse['config']; providersList: ProviderDetails[]; extensionsList: FixedExtensionEntry[]; + extensionWarnings: string[]; upsert: (key: string, value: unknown, is_secret: boolean) => Promise; read: (key: string, is_secret: boolean) => Promise; remove: (key: string, is_secret: boolean) => Promise; @@ -62,6 +63,7 @@ export const ConfigProvider: React.FC = ({ children }) => { const [config, setConfig] = useState({}); const [providersList, setProvidersList] = useState([]); const [extensionsList, setExtensionsList] = useState([]); + const [extensionWarnings, setExtensionWarnings] = useState([]); const reloadConfig = useCallback(async () => { const response = await readAllConfig(); @@ -116,6 +118,7 @@ export const ConfigProvider: React.FC = ({ children }) => { const extensionResponse: ExtensionResponse = result.data!; setExtensionsList(extensionResponse.extensions); + setExtensionWarnings(extensionResponse.warnings || []); return extensionResponse.extensions; }, [extensionsList]); @@ -216,6 +219,7 @@ export const ConfigProvider: React.FC = ({ children }) => { try { const extensionsResponse = await apiGetExtensions(); setExtensionsList(extensionsResponse.data?.extensions || []); + setExtensionWarnings(extensionsResponse.data?.warnings || []); } catch (error) { console.error('Failed to load extensions:', error); } @@ -244,6 +248,7 @@ export const ConfigProvider: React.FC = ({ children }) => { config, providersList, extensionsList, + extensionWarnings, upsert, read, remove, @@ -260,6 +265,7 @@ export const ConfigProvider: React.FC = ({ children }) => { config, providersList, extensionsList, + extensionWarnings, upsert, read, remove, diff --git a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx index 309dd3223f7c..b32bfb6bfc0c 100644 --- a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; import { Button } from '../../ui/button'; -import { Plus } from 'lucide-react'; +import { Plus, AlertTriangle } from 'lucide-react'; import { GPSIcon } from '../../ui/icons'; import { useConfig, FixedExtensionEntry } from '../../ConfigContext'; import ExtensionList from './subcomponents/ExtensionList'; @@ -38,7 +38,7 @@ export default function ExtensionsSection({ onModalClose, searchTerm = '', }: ExtensionSectionProps) { - const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig(); + const { getExtensions, addExtension, removeExtension, extensionsList, extensionWarnings } = useConfig(); const [selectedExtension, setSelectedExtension] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); @@ -234,6 +234,22 @@ export default function ExtensionsSection({ return (
+ {/* Unsupported extension warnings */} + {extensionWarnings.length > 0 && ( +
+
+ +
+ {extensionWarnings.map((warning, index) => ( +

0 ? 'mt-1' : ''}> + {warning} +

+ ))} +
+
+
+ )} + { @@ -62,7 +62,6 @@ export default function ExtensionInfoFields({ }} options={[ { value: 'stdio', label: 'Standard IO (STDIO)' }, - { value: 'sse', label: 'Server-Sent Events (SSE)' }, { value: 'streamable_http', label: 'Streamable HTTP' }, ]} isSearchable={false} diff --git a/ui/desktop/src/components/settings/extensions/utils.test.ts b/ui/desktop/src/components/settings/extensions/utils.test.ts index fc5b5e4c4487..ef96235a6265 100644 --- a/ui/desktop/src/components/settings/extensions/utils.test.ts +++ b/ui/desktop/src/components/settings/extensions/utils.test.ts @@ -79,31 +79,6 @@ describe('Extension Utils', () => { }); }); - it('should convert sse extension to form data', () => { - const extension: FixedExtensionEntry = { - type: 'sse', - name: 'sse-extension', - description: 'SSE description', - uri: 'http://localhost:8080/events', - enabled: false, - env_keys: ['TOKEN'], - }; - - const formData = extensionToFormData(extension); - - expect(formData).toEqual({ - name: 'sse-extension', - description: 'SSE description', - type: 'sse', - cmd: undefined, - endpoint: 'http://localhost:8080/events', - enabled: false, - timeout: undefined, - envVars: [{ key: 'TOKEN', value: '••••••••', isEdited: false }], - headers: [], - }); - }); - it('should convert streamable_http extension to form data', () => { const extension: FixedExtensionEntry = { type: 'streamable_http', @@ -214,31 +189,6 @@ describe('Extension Utils', () => { }); }); - it('should create sse extension config', () => { - const formData = { - name: 'test-sse', - description: 'Test SSE extension', - type: 'sse' as const, - cmd: '', - endpoint: 'http://localhost:8080/events', - enabled: true, - timeout: 600, - envVars: [{ key: 'TOKEN', value: 'abc123', isEdited: true }], - headers: [], - }; - - const config = createExtensionConfig(formData); - - expect(config).toEqual({ - type: 'sse', - name: 'test-sse', - description: 'Test SSE extension', - timeout: 600, - uri: 'http://localhost:8080/events', - env_keys: ['TOKEN'], - }); - }); - it('should create streamable_http extension config', () => { const formData = { name: 'test-http', diff --git a/ui/desktop/src/components/settings/extensions/utils.ts b/ui/desktop/src/components/settings/extensions/utils.ts index b561ea1437ec..d33e85da2166 100644 --- a/ui/desktop/src/components/settings/extensions/utils.ts +++ b/ui/desktop/src/components/settings/extensions/utils.ts @@ -55,8 +55,7 @@ export function getDefaultFormData(): ExtensionFormData { export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFormData { // Type guard: Check if 'envs' property exists for this variant - const hasEnvs = - extension.type === 'sse' || extension.type === 'streamable_http' || extension.type === 'stdio'; + const hasEnvs = extension.type === 'streamable_http' || extension.type === 'stdio'; // Handle both envs (legacy) and env_keys (new secrets) let envVars = []; @@ -106,7 +105,9 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo : extension.type, cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined, endpoint: - extension.type === 'sse' || extension.type === 'streamable_http' ? extension.uri : undefined, + extension.type === 'streamable_http' || extension.type === 'sse' + ? (extension.uri ?? undefined) + : undefined, enabled: extension.enabled, timeout: 'timeout' in extension ? (extension.timeout ?? undefined) : undefined, envVars, @@ -134,15 +135,6 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon timeout: formData.timeout, ...(env_keys.length > 0 ? { env_keys } : {}), }; - } else if (formData.type === 'sse') { - return { - type: 'sse', - name: formData.name, - description: formData.description, - timeout: formData.timeout, - uri: formData.endpoint || '', - ...(env_keys.length > 0 ? { env_keys } : {}), - }; } else if (formData.type === 'streamable_http') { // Extract headers const headers = formData.headers diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index 8862b5c12d69..5206339df9aa 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -106,6 +106,19 @@ export const initializeSystem = async ( const extensionLoadingPromises = enabledExtensions.map(async (extensionConfig) => { const extensionName = extensionConfig.name; + // SSE is unsupported - fail immediately without calling the backend + if (extensionConfig.type === 'sse') { + const errMsg = 'SSE is unsupported, migrate to streamable_http'; + extensionStatuses.set(extensionName, { + name: extensionName, + status: 'error', + error: errMsg, + recoverHints: createExtensionRecoverHints(errMsg), + }); + updateToast(); + return; + } + try { await addToAgentOnStartup({ extensionConfig,