feat: upgrade to rmcp 0.12.0 and sacp 10.0.0 by removing SSE transport (#6304)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Generated
+67
-71
@@ -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"
|
||||
|
||||
+2
-1
@@ -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"
|
||||
|
||||
@@ -78,7 +78,7 @@ impl Evaluation for ComputerControllerScript {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["computercontroller".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ impl Evaluation for ComputerControllerWebScrape {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["computercontroller".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ impl Evaluation for DeveloperCreateFile {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ impl Evaluation for DeveloperListFiles {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ impl Evaluation for SimpleRepoCloneTest {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ impl Evaluation for DeveloperImage {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ impl Evaluation for DeveloperSearchReplace {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl Evaluation for MemoryRememberMemory {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["memory".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ pub struct EvalMetric {
|
||||
pub struct ExtensionRequirements {
|
||||
pub builtin: Vec<String>,
|
||||
pub external: Vec<String>,
|
||||
pub remote: Vec<String>,
|
||||
pub streamable_http: Vec<String>,
|
||||
}
|
||||
|
||||
#[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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ impl Evaluation for FlappyBird {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ impl Evaluation for GooseWiki {
|
||||
ExtensionRequirements {
|
||||
builtin: vec!["developer".to_string()],
|
||||
external: Vec::new(),
|
||||
remote: Vec::new(),
|
||||
streamable_http: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -527,16 +527,6 @@ enum Command {
|
||||
)]
|
||||
extensions: Vec<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// Add streamable HTTP extensions with a URL
|
||||
#[arg(
|
||||
long = "with-streamable-http-extension",
|
||||
@@ -705,16 +695,6 @@ enum Command {
|
||||
)]
|
||||
extensions: Vec<String>,
|
||||
|
||||
/// 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<String>,
|
||||
|
||||
/// 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,
|
||||
|
||||
@@ -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<ExtensionConfig, String> {
|
||||
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<u32>) -> 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::<PermissionOptionKind>(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<CallToolResult>) -> Vec<Tool
|
||||
.content
|
||||
.iter()
|
||||
.filter_map(|content| match &content.raw {
|
||||
RawContent::Text(val) => 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<AuthenticateResponse>| 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<String>) -> 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"
|
||||
)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -625,6 +625,10 @@ pub async fn configure_provider_dialog() -> anyhow::Result<bool> {
|
||||
/// 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::<u64>() {
|
||||
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::<String>() {
|
||||
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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@ pub struct SessionBuilderConfig {
|
||||
pub no_session: bool,
|
||||
/// List of stdio extension commands to add
|
||||
pub extensions: Vec<String>,
|
||||
/// List of remote extension commands to add
|
||||
pub remote_extensions: Vec<String>,
|
||||
/// List of streamable HTTP extension commands to add
|
||||
pub streamable_http_extensions: Vec<String>,
|
||||
/// 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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1330,6 +1330,7 @@ impl ServerHandler for ComputerControllerServer {
|
||||
Ok(ListResourcesResult {
|
||||
resources,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -400,6 +400,7 @@ impl ServerHandler for DeveloperServer {
|
||||
std::future::ready(Ok(ListPromptsResult {
|
||||
prompts,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ use utoipa::ToSchema;
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct ExtensionResponse {
|
||||
pub extensions: Vec<ExtensionEntry>,
|
||||
#[serde(default)]
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, ToSchema)]
|
||||
@@ -254,7 +256,11 @@ pub async fn read_config(
|
||||
)]
|
||||
pub async fn get_extensions() -> Result<Json<ExtensionResponse>, 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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -302,6 +302,7 @@ impl McpClientTrait for ChatRecallClient {
|
||||
Ok(ListToolsResult {
|
||||
tools: Self::get_tools(),
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -835,6 +835,7 @@ impl McpClientTrait for CodeExecutionClient {
|
||||
}),
|
||||
],
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>,
|
||||
// NOTE: set timeout to be optional for compatibility.
|
||||
// However, new configurations should include this field.
|
||||
timeout: Option<u64>,
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
available_tools: Vec<String>,
|
||||
uri: Option<String>,
|
||||
},
|
||||
/// Standard I/O client with command and arguments
|
||||
#[serde(rename = "stdio")]
|
||||
@@ -379,19 +370,6 @@ impl Default for ExtensionConfig {
|
||||
}
|
||||
|
||||
impl ExtensionConfig {
|
||||
pub fn sse<S: Into<String>, T: Into<u64>>(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<S: Into<String>, T: Into<u64>>(
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -481,25 +480,10 @@ impl ExtensionManager {
|
||||
let mut temp_dir = None;
|
||||
|
||||
let client: Box<dyn McpClientTrait> = match &config {
|
||||
ExtensionConfig::Sse { uri, timeout, .. } => {
|
||||
let transport = SseClientTransport::start(uri.to_string()).await.map_err(
|
||||
|transport_error| {
|
||||
ClientInitializeError::transport::<SseClientTransport<reqwest::Client>>(
|
||||
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,
|
||||
@@ -1243,8 +1227,8 @@ impl ExtensionManager {
|
||||
description
|
||||
}
|
||||
}
|
||||
ExtensionConfig::Sse { .. } => "SSE extension (unsupported)",
|
||||
ExtensionConfig::Platform { description, .. }
|
||||
| ExtensionConfig::Sse { description, .. }
|
||||
| ExtensionConfig::StreamableHttp { description, .. }
|
||||
| ExtensionConfig::Stdio { description, .. }
|
||||
| ExtensionConfig::Frontend { description, .. }
|
||||
@@ -1419,6 +1403,7 @@ mod tests {
|
||||
),
|
||||
],
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -410,6 +410,7 @@ impl McpClientTrait for ExtensionManagerClient {
|
||||
Ok(ListToolsResult {
|
||||
tools: self.get_tools().await,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -292,6 +292,7 @@ impl McpClientTrait for SkillsClient {
|
||||
Ok(ListToolsResult {
|
||||
tools,
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -193,6 +193,7 @@ impl McpClientTrait for TodoClient {
|
||||
Ok(ListToolsResult {
|
||||
tools: Self::get_tools(),
|
||||
next_cursor: None,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ fn get_extensions_map() -> IndexMap<String, ExtensionEntry> {
|
||||
let mut extensions_map = IndexMap::with_capacity(raw.len());
|
||||
for (k, v) in raw {
|
||||
match (k, serde_yaml::from_value::<ExtensionEntry>(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<ExtensionConfig> {
|
||||
.map(|ext| ext.config)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_warnings() -> Vec<String> {
|
||||
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::<ExtensionEntry>(v))
|
||||
{
|
||||
if matches!(entry.config, ExtensionConfig::Sse { .. }) {
|
||||
warnings.push(format!(
|
||||
"'{}': SSE is unsupported, migrate to streamable_http",
|
||||
key
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
warnings
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String>,
|
||||
uri: String,
|
||||
#[serde(default)]
|
||||
envs: Envs,
|
||||
#[serde(default)]
|
||||
env_keys: Vec<String>,
|
||||
timeout: Option<u64>,
|
||||
#[serde(default)]
|
||||
bundled: Option<bool>,
|
||||
#[serde(default)]
|
||||
available_tools: Vec<String>,
|
||||
},
|
||||
#[serde(rename = "stdio")]
|
||||
Stdio {
|
||||
name: String,
|
||||
@@ -127,15 +111,7 @@ macro_rules! map_recipe_extensions {
|
||||
impl From<RecipeExtensionConfigInternal> 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::<Vec<RecipeExtensionConfigInternal>>::deserialize(deserializer)?;
|
||||
Ok(remotes.map(|items| {
|
||||
items
|
||||
.into_iter()
|
||||
.map(ExtensionConfig::from)
|
||||
.collect::<Vec<_>>()
|
||||
}))
|
||||
Ok(remotes.map(|items| items.into_iter().map(ExtensionConfig::from).collect()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -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<F, Fut>(
|
||||
)
|
||||
.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<F, Fut>(
|
||||
.run_until({
|
||||
let updates = updates.clone();
|
||||
move |cx: JrConnectionCx<ClientToAgent>| 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(),
|
||||
|
||||
+14
-30
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+10
-30
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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<string>;
|
||||
bundled?: boolean | null;
|
||||
description: string;
|
||||
env_keys?: Array<string>;
|
||||
envs?: Envs;
|
||||
/**
|
||||
* The name used to identify this extension
|
||||
*/
|
||||
name: string;
|
||||
timeout?: number | null;
|
||||
type: 'sse';
|
||||
uri: string;
|
||||
uri?: string | null;
|
||||
} | {
|
||||
args: Array<string>;
|
||||
available_tools?: Array<string>;
|
||||
@@ -351,6 +343,7 @@ export type ExtensionQuery = {
|
||||
|
||||
export type ExtensionResponse = {
|
||||
extensions: Array<ExtensionEntry>;
|
||||
warnings?: Array<string>;
|
||||
};
|
||||
|
||||
export type FrontendToolRequest = {
|
||||
@@ -667,6 +660,9 @@ export type RawImageContent = {
|
||||
};
|
||||
|
||||
export type RawResource = {
|
||||
_meta?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
description?: string;
|
||||
icons?: Array<Icon>;
|
||||
mimeType?: string;
|
||||
|
||||
@@ -31,6 +31,7 @@ interface ConfigContextType {
|
||||
config: ConfigResponse['config'];
|
||||
providersList: ProviderDetails[];
|
||||
extensionsList: FixedExtensionEntry[];
|
||||
extensionWarnings: string[];
|
||||
upsert: (key: string, value: unknown, is_secret: boolean) => Promise<void>;
|
||||
read: (key: string, is_secret: boolean) => Promise<unknown>;
|
||||
remove: (key: string, is_secret: boolean) => Promise<void>;
|
||||
@@ -62,6 +63,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
const [config, setConfig] = useState<ConfigResponse['config']>({});
|
||||
const [providersList, setProvidersList] = useState<ProviderDetails[]>([]);
|
||||
const [extensionsList, setExtensionsList] = useState<FixedExtensionEntry[]>([]);
|
||||
const [extensionWarnings, setExtensionWarnings] = useState<string[]>([]);
|
||||
|
||||
const reloadConfig = useCallback(async () => {
|
||||
const response = await readAllConfig();
|
||||
@@ -116,6 +118,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ 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<ConfigProviderProps> = ({ 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<ConfigProviderProps> = ({ children }) => {
|
||||
config,
|
||||
providersList,
|
||||
extensionsList,
|
||||
extensionWarnings,
|
||||
upsert,
|
||||
read,
|
||||
remove,
|
||||
@@ -260,6 +265,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
config,
|
||||
providersList,
|
||||
extensionsList,
|
||||
extensionWarnings,
|
||||
upsert,
|
||||
read,
|
||||
remove,
|
||||
|
||||
@@ -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<FixedExtensionEntry | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||
@@ -234,6 +234,22 @@ export default function ExtensionsSection({
|
||||
return (
|
||||
<section id="extensions">
|
||||
<div className="">
|
||||
{/* Unsupported extension warnings */}
|
||||
{extensionWarnings.length > 0 && (
|
||||
<div className="mb-4 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<div className="text-sm text-yellow-500">
|
||||
{extensionWarnings.map((warning, index) => (
|
||||
<p key={index} className={index > 0 ? 'mt-1' : ''}>
|
||||
{warning}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ExtensionList
|
||||
extensions={extensions}
|
||||
onToggle={handleExtensionToggle}
|
||||
|
||||
@@ -10,7 +10,7 @@ type BundledExtension = {
|
||||
display_name?: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
type: 'builtin' | 'stdio' | 'sse';
|
||||
type: 'builtin' | 'stdio' | 'streamable_http';
|
||||
cmd?: string;
|
||||
args?: string[];
|
||||
uri?: string;
|
||||
@@ -80,7 +80,7 @@ export async function syncBundledExtensions(
|
||||
bundled: true,
|
||||
};
|
||||
break;
|
||||
case 'sse':
|
||||
case 'streamable_http':
|
||||
extConfig = {
|
||||
type: bundledExt.type,
|
||||
name: bundledExt.name,
|
||||
|
||||
@@ -64,21 +64,6 @@ function getStdioConfig(
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an extension config for SSE from the deeplink URL
|
||||
*/
|
||||
function getSseConfig(remoteUrl: string, name: string, description: string, timeout: number) {
|
||||
const config: ExtensionConfig = {
|
||||
name,
|
||||
type: 'sse',
|
||||
uri: remoteUrl,
|
||||
description,
|
||||
timeout: timeout,
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an extension config for Streamable HTTP from the deeplink URL
|
||||
*/
|
||||
@@ -150,9 +135,6 @@ export async function addExtensionFromDeepLink(
|
||||
|
||||
const cmd = parsedUrl.searchParams.get('cmd');
|
||||
const remoteUrl = parsedUrl.searchParams.get('url');
|
||||
// Support both 'transport' and 'type' parameters for consistency
|
||||
const transportType =
|
||||
parsedUrl.searchParams.get('transport') || parsedUrl.searchParams.get('type') || 'sse'; // Default to SSE for backward compatibility
|
||||
|
||||
const headerParams = parsedUrl.searchParams.getAll('header');
|
||||
const headers =
|
||||
@@ -178,9 +160,7 @@ export async function addExtensionFromDeepLink(
|
||||
: undefined;
|
||||
|
||||
const baseConfig = remoteUrl
|
||||
? transportType === 'streamable_http'
|
||||
? getStreamableHttpConfig(remoteUrl, name, description || '', timeout, headers, envs)
|
||||
: getSseConfig(remoteUrl, name, description || '', timeout)
|
||||
? getStreamableHttpConfig(remoteUrl, name, description || '', timeout, headers, envs)
|
||||
: getStdioConfig(cmd!, parsedUrl, name, description || '', timeout);
|
||||
|
||||
const config = {
|
||||
|
||||
@@ -48,10 +48,10 @@ export default function ExtensionInfoFields({
|
||||
label:
|
||||
type === 'stdio'
|
||||
? 'STDIO'
|
||||
: type === 'sse'
|
||||
? 'SSE'
|
||||
: type === 'streamable_http'
|
||||
? 'HTTP'
|
||||
: type === 'streamable_http'
|
||||
? 'HTTP'
|
||||
: type === 'sse'
|
||||
? 'SSE (unsupported)'
|
||||
: type.toUpperCase(),
|
||||
}}
|
||||
onChange={(newValue: unknown) => {
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user