diff --git a/crates/goose-sdk-types/src/custom_requests.rs b/crates/goose-sdk-types/src/custom_requests.rs index 672ff8b0..3ef2df1c 100644 --- a/crates/goose-sdk-types/src/custom_requests.rs +++ b/crates/goose-sdk-types/src/custom_requests.rs @@ -1,3 +1,4 @@ +use agent_client_protocol::schema::McpServer; use agent_client_protocol::{JsonRpcRequest, JsonRpcResponse}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -147,30 +148,104 @@ pub struct DeleteSessionRequest { pub session_id: String, } +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum GooseExtension { + Builtin { + name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + display_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + timeout: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + bundled: Option, + }, + Platform { + name: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + display_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + bundled: Option, + }, + Mcp { + server: McpServer, + #[serde(default, rename = "envKeys", skip_serializing_if = "Vec::is_empty")] + env_keys: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + timeout: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + socket: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + bundled: Option, + }, +} + +impl Default for GooseExtension { + fn default() -> Self { + Self::Builtin { + name: String::new(), + description: None, + display_name: None, + timeout: None, + bundled: None, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct GooseExtensionEntry { + pub extension: GooseExtension, + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub config_key: Option, +} + +/// List Goose-owned extension definitions available to configure or enable. +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] +#[request( + method = "_goose/unstable/extensions/available", + response = GetAvailableExtensionsResponse +)] +pub struct GetAvailableExtensionsRequest {} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] +#[serde(rename_all = "camelCase")] +pub struct GetAvailableExtensionsResponse { + pub extensions: Vec, +} + /// List configured extensions and any warnings. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request(method = "_goose/unstable/config/extensions/list", response = GetExtensionsResponse)] -pub struct GetExtensionsRequest {} +#[request( + method = "_goose/unstable/config/extensions/list", + response = GetConfigExtensionsResponse +)] +pub struct GetConfigExtensionsRequest {} /// List configured extensions and any warnings. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcResponse)] -pub struct GetExtensionsResponse { - /// Array of ExtensionEntry objects with `enabled` flag, `configKey`, and flattened config details. - pub extensions: Vec, +pub struct GetConfigExtensionsResponse { + pub extensions: Vec, + #[serde(default)] pub warnings: Vec, } +pub type GetExtensionsRequest = GetConfigExtensionsRequest; +pub type GetExtensionsResponse = GetConfigExtensionsResponse; + /// Persist a new extension to the user's global goose config. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] #[request(method = "_goose/unstable/config/extensions/add", response = EmptyResponse)] #[serde(rename_all = "camelCase")] pub struct AddConfigExtensionRequest { - pub name: String, - /// Extension configuration. Must be a JSON object matching one of the - /// `ExtensionConfig` variants (e.g. `stdio`, `streamable_http`, `builtin`). - /// `name` and `enabled` are injected server-side. - #[serde(default)] - pub extension_config: serde_json::Value, + pub extension: GooseExtension, #[serde(default)] pub enabled: bool, } @@ -183,11 +258,14 @@ pub struct RemoveConfigExtensionRequest { pub config_key: String, } -/// Toggle the `enabled` flag for a persisted extension in the user's global goose config. +/// Set the `enabled` flag for a persisted extension in the user's global goose config. #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema, JsonRpcRequest)] -#[request(method = "_goose/unstable/config/extensions/toggle", response = EmptyResponse)] +#[request( + method = "_goose/unstable/config/extensions/set-enabled", + response = EmptyResponse +)] #[serde(rename_all = "camelCase")] -pub struct ToggleConfigExtensionRequest { +pub struct SetConfigExtensionEnabledRequest { pub config_key: String, pub enabled: bool, } diff --git a/crates/goose/acp-meta.json b/crates/goose/acp-meta.json index e72f4370..7be80cc9 100644 --- a/crates/goose/acp-meta.json +++ b/crates/goose/acp-meta.json @@ -42,8 +42,13 @@ }, { "method": "_goose/unstable/config/extensions/list", - "requestType": "GetExtensionsRequest_unstable", - "responseType": "GetExtensionsResponse_unstable" + "requestType": "GetConfigExtensionsRequest_unstable", + "responseType": "GetConfigExtensionsResponse_unstable" + }, + { + "method": "_goose/unstable/extensions/available", + "requestType": "GetAvailableExtensionsRequest_unstable", + "responseType": "GetAvailableExtensionsResponse_unstable" }, { "method": "_goose/unstable/config/extensions/add", @@ -56,8 +61,8 @@ "responseType": "EmptyResponse" }, { - "method": "_goose/unstable/config/extensions/toggle", - "requestType": "ToggleConfigExtensionRequest_unstable", + "method": "_goose/unstable/config/extensions/set-enabled", + "requestType": "SetConfigExtensionEnabledRequest_unstable", "responseType": "EmptyResponse" }, { diff --git a/crates/goose/acp-schema.json b/crates/goose/acp-schema.json index 5980ff34..ebad7d1b 100644 --- a/crates/goose/acp-schema.json +++ b/crates/goose/acp-schema.json @@ -224,44 +224,419 @@ "x-side": "agent", "x-method": "session/delete" }, - "GetExtensionsRequest_unstable": { + "GetConfigExtensionsRequest_unstable": { "type": "object", "description": "List configured extensions and any warnings.", "x-side": "agent", "x-method": "_goose/unstable/config/extensions/list" }, - "GetExtensionsResponse_unstable": { + "GetConfigExtensionsResponse_unstable": { "type": "object", "properties": { "extensions": { "type": "array", - "items": {}, - "description": "Array of ExtensionEntry objects with `enabled` flag, `configKey`, and flattened config details." + "items": { + "$ref": "#/$defs/GooseExtensionEntry" + } }, "warnings": { "type": "array", "items": { "type": "string" - } + }, + "default": [] } }, "required": [ - "extensions", - "warnings" + "extensions" ], "description": "List configured extensions and any warnings.", "x-side": "agent", "x-method": "_goose/unstable/config/extensions/list" }, - "AddConfigExtensionRequest_unstable": { + "GooseExtensionEntry": { + "type": "object", + "properties": { + "extension": { + "$ref": "#/$defs/GooseExtension" + }, + "enabled": { + "type": "boolean" + }, + "configKey": { + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "extension", + "enabled" + ] + }, + "GooseExtension": { + "oneOf": [ + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "timeout": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "bundled": { + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "const": "builtin" + } + }, + "required": [ + "type", + "name" + ] + }, + { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "display_name": { + "type": [ + "string", + "null" + ] + }, + "bundled": { + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "const": "platform" + } + }, + "required": [ + "type", + "name" + ] + }, + { + "type": "object", + "properties": { + "server": { + "$ref": "#/$defs/McpServer" + }, + "envKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "description": { + "type": [ + "string", + "null" + ] + }, + "timeout": { + "type": [ + "integer", + "null" + ], + "minimum": 0 + }, + "socket": { + "type": [ + "string", + "null" + ] + }, + "bundled": { + "type": [ + "boolean", + "null" + ] + }, + "type": { + "type": "string", + "const": "mcp" + } + }, + "required": [ + "type", + "server" + ] + } + ] + }, + "McpServer": { + "anyOf": [ + { + "$ref": "#/$defs/McpServerHttp", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "http" + } + }, + "required": [ + "type" + ], + "description": "HTTP transport configuration\n\nOnly available when the Agent capabilities indicate `mcp_capabilities.http` is `true`." + }, + { + "$ref": "#/$defs/McpServerSse", + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "sse" + } + }, + "required": [ + "type" + ], + "description": "SSE transport configuration\n\nOnly available when the Agent capabilities indicate `mcp_capabilities.sse` is `true`." + }, + { + "$ref": "#/$defs/McpServerStdio", + "description": "Stdio transport configuration\n\nAll Agents MUST support this transport." + } + ], + "description": "Configuration for connecting to an MCP (Model Context Protocol) server.\n\nMCP servers provide tools and context that the agent can use when\nprocessing prompts.\n\nSee protocol docs: [MCP Servers](https://agentclientprotocol.com/protocol/session-setup#mcp-servers)" + }, + "HttpHeader": { "type": "object", "properties": { "name": { - "type": "string" + "type": "string", + "description": "The name of the HTTP header." }, - "extensionConfig": { - "description": "Extension configuration. Must be a JSON object matching one of the\n`ExtensionConfig` variants (e.g. `stdio`, `streamable_http`, `builtin`).\n`name` and `enabled` are injected server-side.", - "default": null + "value": { + "type": "string", + "description": "The value to set for the HTTP header." + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "name", + "value" + ], + "description": "An HTTP header to set when making requests to the MCP server." + }, + "McpServerHttp": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Human-readable name identifying this MCP server." + }, + "url": { + "type": "string", + "description": "URL to the MCP server." + }, + "headers": { + "type": "array", + "items": { + "$ref": "#/$defs/HttpHeader" + }, + "description": "HTTP headers to set when making requests to the MCP server." + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + }, + "type": { + "type": "string", + "const": "http" + } + }, + "required": [ + "type", + "name", + "url", + "headers" + ], + "description": "HTTP transport configuration for MCP." + }, + "McpServerSse": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Human-readable name identifying this MCP server." + }, + "url": { + "type": "string", + "description": "URL to the MCP server." + }, + "headers": { + "type": "array", + "items": { + "$ref": "#/$defs/HttpHeader" + }, + "description": "HTTP headers to set when making requests to the MCP server." + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + }, + "type": { + "type": "string", + "const": "sse" + } + }, + "required": [ + "type", + "name", + "url", + "headers" + ], + "description": "SSE transport configuration for MCP." + }, + "McpServerStdio": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Human-readable name identifying this MCP server." + }, + "command": { + "type": "string", + "description": "Path to the MCP server executable." + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Command-line arguments to pass to the MCP server." + }, + "env": { + "type": "array", + "items": { + "$ref": "#/$defs/EnvVariable" + }, + "description": "Environment variables to set when launching the MCP server." + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "name", + "command", + "args", + "env" + ], + "description": "Stdio transport configuration for MCP." + }, + "EnvVariable": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the environment variable." + }, + "value": { + "type": "string", + "description": "The value to set for the environment variable." + }, + "_meta": { + "type": [ + "object", + "null" + ], + "additionalProperties": {}, + "description": "The _meta property is reserved by ACP to allow clients and agents to attach additional\nmetadata to their interactions. Implementations MUST NOT make assumptions about values at\nthese keys.\n\nSee protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility)" + } + }, + "required": [ + "name", + "value" + ], + "description": "An environment variable to set when launching an MCP server." + }, + "GetAvailableExtensionsRequest_unstable": { + "type": "object", + "description": "List Goose-owned extension definitions available to configure or enable.", + "x-side": "agent", + "x-method": "_goose/unstable/extensions/available" + }, + "GetAvailableExtensionsResponse_unstable": { + "type": "object", + "properties": { + "extensions": { + "type": "array", + "items": { + "$ref": "#/$defs/GooseExtension" + } + } + }, + "required": [ + "extensions" + ], + "x-side": "agent", + "x-method": "_goose/unstable/extensions/available" + }, + "AddConfigExtensionRequest_unstable": { + "type": "object", + "properties": { + "extension": { + "$ref": "#/$defs/GooseExtension" }, "enabled": { "type": "boolean", @@ -269,7 +644,7 @@ } }, "required": [ - "name" + "extension" ], "description": "Persist a new extension to the user's global goose config.", "x-side": "agent", @@ -289,7 +664,7 @@ "x-side": "agent", "x-method": "_goose/unstable/config/extensions/remove" }, - "ToggleConfigExtensionRequest_unstable": { + "SetConfigExtensionEnabledRequest_unstable": { "type": "object", "properties": { "configKey": { @@ -303,9 +678,9 @@ "configKey", "enabled" ], - "description": "Toggle the `enabled` flag for a persisted extension in the user's global goose config.", + "description": "Set the `enabled` flag for a persisted extension in the user's global goose config.", "x-side": "agent", - "x-method": "_goose/unstable/config/extensions/toggle" + "x-method": "_goose/unstable/config/extensions/set-enabled" }, "GetSessionExtensionsRequest_unstable": { "type": "object", @@ -545,9 +920,8 @@ "integer", "null" ], - "format": "uint", - "minimum": 0, - "description": "Context window size in tokens." + "description": "Context window size in tokens.", + "minimum": 0 }, "reasoning": { "type": [ @@ -2962,11 +3336,20 @@ { "allOf": [ { - "$ref": "#/$defs/GetExtensionsRequest_unstable" + "$ref": "#/$defs/GetConfigExtensionsRequest_unstable" } ], "description": "Params for _goose/unstable/config/extensions/list", - "title": "GetExtensionsRequest_unstable" + "title": "GetConfigExtensionsRequest_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetAvailableExtensionsRequest_unstable" + } + ], + "description": "Params for _goose/unstable/extensions/available", + "title": "GetAvailableExtensionsRequest_unstable" }, { "allOf": [ @@ -2989,11 +3372,11 @@ { "allOf": [ { - "$ref": "#/$defs/ToggleConfigExtensionRequest_unstable" + "$ref": "#/$defs/SetConfigExtensionEnabledRequest_unstable" } ], - "description": "Params for _goose/unstable/config/extensions/toggle", - "title": "ToggleConfigExtensionRequest_unstable" + "description": "Params for _goose/unstable/config/extensions/set-enabled", + "title": "SetConfigExtensionEnabledRequest_unstable" }, { "allOf": [ @@ -3474,10 +3857,18 @@ { "allOf": [ { - "$ref": "#/$defs/GetExtensionsResponse_unstable" + "$ref": "#/$defs/GetConfigExtensionsResponse_unstable" } ], - "title": "GetExtensionsResponse_unstable" + "title": "GetConfigExtensionsResponse_unstable" + }, + { + "allOf": [ + { + "$ref": "#/$defs/GetAvailableExtensionsResponse_unstable" + } + ], + "title": "GetAvailableExtensionsResponse_unstable" }, { "allOf": [ diff --git a/crates/goose/src/acp/server/custom_dispatch.rs b/crates/goose/src/acp/server/custom_dispatch.rs index 65c23aa8..fe0df904 100644 --- a/crates/goose/src/acp/server/custom_dispatch.rs +++ b/crates/goose/src/acp/server/custom_dispatch.rs @@ -75,11 +75,18 @@ impl GooseAcpAgent { self.on_delete_session(req).await } - #[custom_method(GetExtensionsRequest)] - async fn dispatch_get_extensions( + #[custom_method(GetConfigExtensionsRequest)] + async fn dispatch_get_config_extensions( &self, - ) -> Result { - self.on_get_extensions().await + ) -> Result { + self.on_get_config_extensions().await + } + + #[custom_method(GetAvailableExtensionsRequest)] + async fn dispatch_get_available_extensions( + &self, + ) -> Result { + self.on_get_available_extensions().await } #[custom_method(AddConfigExtensionRequest)] @@ -98,12 +105,12 @@ impl GooseAcpAgent { self.on_remove_config_extension(req).await } - #[custom_method(ToggleConfigExtensionRequest)] - async fn dispatch_toggle_config_extension( + #[custom_method(SetConfigExtensionEnabledRequest)] + async fn dispatch_set_config_extension_enabled( &self, - req: ToggleConfigExtensionRequest, + req: SetConfigExtensionEnabledRequest, ) -> Result { - self.on_toggle_config_extension(req).await + self.on_set_config_extension_enabled(req).await } #[custom_method(GetSessionExtensionsRequest)] diff --git a/crates/goose/src/acp/server/extensions.rs b/crates/goose/src/acp/server/extensions.rs index b45e0407..ff58cb21 100644 --- a/crates/goose/src/acp/server/extensions.rs +++ b/crates/goose/src/acp/server/extensions.rs @@ -1,4 +1,7 @@ use super::*; +use crate::agents::extension::Envs; +use crate::config::extensions::ExtensionEntry; +use agent_client_protocol::schema::{HttpHeader, McpServer, McpServerHttp, McpServerStdio}; impl GooseAcpAgent { pub(super) async fn on_add_extension( @@ -30,9 +33,9 @@ impl GooseAcpAgent { Ok(EmptyResponse {}) } - pub(super) async fn on_get_extensions( + pub(super) async fn on_get_config_extensions( &self, - ) -> Result { + ) -> Result { let extensions = crate::config::extensions::get_all_extensions() .into_iter() .filter(|ext| { @@ -40,51 +43,46 @@ impl GooseAcpAgent { }) .collect::>(); let warnings = crate::config::extensions::get_warnings(); - let extensions_json = extensions + let extensions = extensions .into_iter() - .map(|e| { - let config_key = e.config.key(); - let mut value = serde_json::to_value(&e)?; - if let Some(obj) = value.as_object_mut() { - obj.insert( - "config_key".to_string(), - serde_json::Value::String(config_key), - ); - } - Ok::<_, serde_json::Error>(value) - }) - .collect::, _>>() - .internal_err()?; - Ok(GetExtensionsResponse { - extensions: extensions_json, + .map(config_entry_to_goose_entry) + .collect::, _>>()? + .into_iter() + .flatten() + .collect::>(); + Ok(GetConfigExtensionsResponse { + extensions, warnings, }) } + pub(super) async fn on_get_available_extensions( + &self, + ) -> Result { + let extensions = crate::config::get_available_extensions() + .into_iter() + .map(|config| config_to_goose_extension(&config)) + .collect::, _>>()? + .into_iter() + .flatten() + .collect::>(); + + Ok(GetAvailableExtensionsResponse { extensions }) + } + pub(super) async fn on_add_config_extension( &self, req: AddConfigExtensionRequest, ) -> Result { - let mut obj = match req.extension_config { - serde_json::Value::Object(obj) => obj, - _ => { - return Err(agent_client_protocol::Error::invalid_params() - .data("extensionConfig must be a JSON object")); - } - }; - obj.insert( - "name".to_string(), - serde_json::Value::String(req.name.clone()), - ); + let conversion = goose_extension_to_config(req.extension)?; - let config: crate::agents::ExtensionConfig = - serde_json::from_value(serde_json::Value::Object(obj)).map_err(|e| { - agent_client_protocol::Error::invalid_params().data(format!("bad config: {e}")) - })?; + Config::global() + .set_secret_values(&conversion.secret_updates) + .internal_err_ctx("Failed to save extension env secrets")?; - crate::config::extensions::set_extension(crate::config::extensions::ExtensionEntry { + crate::config::extensions::set_extension(ExtensionEntry { enabled: req.enabled, - config, + config: conversion.config, }); Ok(EmptyResponse {}) } @@ -93,25 +91,21 @@ impl GooseAcpAgent { &self, req: RemoveConfigExtensionRequest, ) -> Result { - let keys = crate::config::extensions::get_all_extension_names(); - if !keys.iter().any(|k| k == &req.config_key) { - return Err(agent_client_protocol::Error::invalid_params() - .data(format!("Extension '{}' not found", req.config_key))); - } crate::config::extensions::remove_extension(&req.config_key); Ok(EmptyResponse {}) } - pub(super) async fn on_toggle_config_extension( + pub(super) async fn on_set_config_extension_enabled( &self, - req: ToggleConfigExtensionRequest, + req: SetConfigExtensionEnabledRequest, ) -> Result { - let keys = crate::config::extensions::get_all_extension_names(); - if !keys.iter().any(|k| k == &req.config_key) { + let updated = + crate::config::extensions::set_extension_enabled(&req.config_key, req.enabled); + if !updated { return Err(agent_client_protocol::Error::invalid_params() .data(format!("Extension '{}' not found", req.config_key))); } - crate::config::extensions::set_extension_enabled(&req.config_key, req.enabled); + Ok(EmptyResponse {}) } @@ -142,3 +136,655 @@ impl GooseAcpAgent { }) } } + +fn config_to_goose_extension( + config: &ExtensionConfig, +) -> Result, agent_client_protocol::Error> { + let extension = match config { + ExtensionConfig::Builtin { + name, + description, + display_name, + timeout, + bundled, + .. + } => GooseExtension::Builtin { + name: name.clone(), + description: empty_string_to_none(description), + display_name: display_name.clone(), + timeout: *timeout, + bundled: *bundled, + }, + ExtensionConfig::Platform { + name, + description, + display_name, + bundled, + .. + } => GooseExtension::Platform { + name: name.clone(), + description: empty_string_to_none(description), + display_name: display_name.clone(), + bundled: *bundled, + }, + ExtensionConfig::Stdio { + name, + description, + cmd, + args, + env_keys, + timeout, + bundled, + .. + } => GooseExtension::Mcp { + server: McpServer::Stdio(McpServerStdio::new(name, cmd).args(args.clone())), + env_keys: env_keys.clone(), + description: empty_string_to_none(description), + timeout: *timeout, + socket: None, + bundled: *bundled, + }, + ExtensionConfig::StreamableHttp { + name, + description, + uri, + env_keys, + headers, + timeout, + socket, + bundled, + .. + } => { + let headers = headers + .iter() + .map(|(key, value)| HttpHeader::new(key, value)) + .collect(); + GooseExtension::Mcp { + server: McpServer::Http(McpServerHttp::new(name, uri).headers(headers)), + env_keys: env_keys.clone(), + description: empty_string_to_none(description), + timeout: *timeout, + socket: socket.clone(), + bundled: *bundled, + } + } + ExtensionConfig::Frontend { .. } + | ExtensionConfig::InlinePython { .. } + | ExtensionConfig::Sse { .. } => return Ok(None), + }; + Ok(Some(extension)) +} + +struct ConfigExtensionConversion { + config: ExtensionConfig, + secret_updates: Vec<(String, serde_json::Value)>, +} + +fn goose_extension_to_config( + extension: GooseExtension, +) -> Result { + let mut secret_updates = Vec::new(); + let config = match extension { + GooseExtension::Builtin { + name, + description, + display_name, + timeout, + bundled, + } => ExtensionConfig::Builtin { + name, + description: description.unwrap_or_default(), + display_name, + timeout, + bundled, + available_tools: Vec::new(), + }, + GooseExtension::Platform { + name, + description, + display_name, + bundled, + } => ExtensionConfig::Platform { + name, + description: description.unwrap_or_default(), + display_name, + bundled, + available_tools: Vec::new(), + }, + GooseExtension::Mcp { + server, + env_keys, + description, + timeout, + socket, + bundled, + } => match server { + McpServer::Stdio(stdio) => { + if socket.is_some() { + return Err(agent_client_protocol::Error::invalid_params() + .data("socket is only supported for streamable_http MCP extensions")); + } + let mut env_keys = env_keys; + for env in stdio.env { + if !env_keys.contains(&env.name) { + env_keys.push(env.name.clone()); + } + secret_updates.push((env.name, serde_json::Value::String(env.value))); + } + ExtensionConfig::Stdio { + name: stdio.name, + description: description.unwrap_or_default(), + cmd: stdio.command.to_string_lossy().to_string(), + args: stdio.args, + envs: Envs::default(), + env_keys, + timeout, + bundled, + available_tools: Vec::new(), + } + } + McpServer::Http(http) => ExtensionConfig::StreamableHttp { + name: http.name, + description: description.unwrap_or_default(), + uri: http.url, + envs: Envs::default(), + env_keys, + headers: http + .headers + .into_iter() + .map(|header| (header.name, header.value)) + .collect(), + timeout, + socket, + bundled, + available_tools: Vec::new(), + }, + McpServer::Sse(_) => { + return Err(agent_client_protocol::Error::invalid_params() + .data("SSE is unsupported, migrate to streamable_http")); + } + _ => { + return Err( + agent_client_protocol::Error::invalid_params().data("unsupported MCP server") + ); + } + }, + }; + Ok(ConfigExtensionConversion { + config, + secret_updates, + }) +} + +fn config_entry_to_goose_entry( + entry: ExtensionEntry, +) -> Result, agent_client_protocol::Error> { + let config_key = entry.config.key(); + let Some(extension) = config_to_goose_extension(&entry.config)? else { + return Ok(None); + }; + Ok(Some(GooseExtensionEntry { + extension, + enabled: entry.enabled, + config_key: Some(config_key), + })) +} + +fn empty_string_to_none(value: &str) -> Option { + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::agents::extension::Envs; + use agent_client_protocol::schema::{McpServer, McpServerSse}; + use std::collections::HashMap; + + #[test] + fn builtin_config_converts_to_goose_builtin_extension() { + let config = ExtensionConfig::Builtin { + name: "developer".to_string(), + description: "Developer tools".to_string(), + display_name: Some("Developer".to_string()), + timeout: Some(30), + bundled: Some(true), + available_tools: vec!["shell".to_string()], + }; + + let extension = config_to_goose_extension(&config) + .expect("conversion should succeed") + .expect("builtin should be supported"); + + let GooseExtension::Builtin { + name, + description, + display_name, + timeout, + bundled, + } = extension + else { + panic!("expected builtin extension"); + }; + + assert_eq!(name, "developer"); + assert_eq!(description.as_deref(), Some("Developer tools")); + assert_eq!(display_name.as_deref(), Some("Developer")); + assert_eq!(timeout, Some(30)); + assert_eq!(bundled, Some(true)); + } + + #[test] + fn platform_config_converts_to_goose_platform_extension() { + let config = ExtensionConfig::Platform { + name: "todo".to_string(), + description: "Todo tools".to_string(), + display_name: Some("Todo".to_string()), + bundled: Some(true), + available_tools: vec!["write_todos".to_string()], + }; + + let extension = config_to_goose_extension(&config) + .expect("conversion should succeed") + .expect("platform should be supported"); + + let GooseExtension::Platform { + name, + description, + display_name, + bundled, + } = extension + else { + panic!("expected platform extension"); + }; + + assert_eq!(name, "todo"); + assert_eq!(description.as_deref(), Some("Todo tools")); + assert_eq!(display_name.as_deref(), Some("Todo")); + assert_eq!(bundled, Some(true)); + } + + #[test] + fn stdio_config_converts_to_goose_mcp_extension_without_literal_envs() { + let config = ExtensionConfig::Stdio { + name: "test-stdio".to_string(), + description: "Test stdio".to_string(), + cmd: "test-command".to_string(), + args: vec!["--flag".to_string(), "value".to_string()], + envs: Envs::new(HashMap::from([( + "SECRET_TOKEN".to_string(), + "literal-secret".to_string(), + )])), + env_keys: vec!["SECRET_TOKEN".to_string()], + timeout: Some(42), + bundled: None, + available_tools: vec![], + }; + + let extension = config_to_goose_extension(&config) + .expect("conversion should succeed") + .expect("stdio should be supported"); + + let GooseExtension::Mcp { + server, + env_keys, + description, + timeout, + socket, + bundled, + } = extension + else { + panic!("expected mcp extension"); + }; + + assert_eq!(env_keys, vec!["SECRET_TOKEN"]); + assert_eq!(description.as_deref(), Some("Test stdio")); + assert_eq!(timeout, Some(42)); + assert_eq!(socket, None); + assert_eq!(bundled, None); + + let McpServer::Stdio(stdio) = server else { + panic!("expected stdio server"); + }; + + assert_eq!(stdio.name, "test-stdio"); + assert_eq!(stdio.command.to_string_lossy(), "test-command"); + assert_eq!(stdio.args, vec!["--flag", "value"]); + assert!(stdio.env.is_empty(), "literal envs should not be exposed"); + } + + #[test] + fn streamable_http_config_converts_to_goose_mcp_extension_without_literal_envs() { + let config = ExtensionConfig::StreamableHttp { + name: "test-http".to_string(), + description: "Test HTTP".to_string(), + uri: "https://example.com/mcp".to_string(), + envs: Envs::new(HashMap::from([( + "API_TOKEN".to_string(), + "literal-secret".to_string(), + )])), + env_keys: vec!["API_TOKEN".to_string()], + headers: HashMap::from([( + "Authorization".to_string(), + "Bearer ${API_TOKEN}".to_string(), + )]), + timeout: Some(99), + socket: Some("@egress.sock".to_string()), + bundled: None, + available_tools: vec![], + }; + + let extension = config_to_goose_extension(&config) + .expect("conversion should succeed") + .expect("streamable http should be supported"); + + let GooseExtension::Mcp { + server, + env_keys, + description, + timeout, + socket, + bundled, + } = extension + else { + panic!("expected mcp extension"); + }; + + assert_eq!(env_keys, vec!["API_TOKEN"]); + assert_eq!(description.as_deref(), Some("Test HTTP")); + assert_eq!(timeout, Some(99)); + assert_eq!(socket.as_deref(), Some("@egress.sock")); + assert_eq!(bundled, None); + + let McpServer::Http(http) = server else { + panic!("expected http server"); + }; + + assert_eq!(http.name, "test-http"); + assert_eq!(http.url, "https://example.com/mcp"); + assert_eq!(http.headers.len(), 1); + assert_eq!(http.headers[0].name, "Authorization"); + assert_eq!(http.headers[0].value, "Bearer ${API_TOKEN}"); + } + + #[test] + fn inline_python_config_is_skipped() { + let config = ExtensionConfig::InlinePython { + name: "python-tools".to_string(), + description: "Python tools".to_string(), + code: "print('hello')".to_string(), + timeout: Some(12), + dependencies: Some(vec!["requests".to_string()]), + available_tools: vec!["fetch".to_string()], + }; + + let extension = config_to_goose_extension(&config).expect("conversion should succeed"); + + assert!(extension.is_none()); + } + + #[test] + fn frontend_config_is_skipped() { + let tool = rmcp::model::Tool::new( + "pick_color", + "Pick a color", + serde_json::json!({ + "type": "object", + "properties": { + "hex": { "type": "string" } + } + }) + .as_object() + .expect("schema should be object") + .clone(), + ); + let config = ExtensionConfig::Frontend { + name: "frontend-tools".to_string(), + description: "Frontend tools".to_string(), + tools: vec![tool], + instructions: Some("Use frontend tools carefully".to_string()), + bundled: None, + available_tools: vec!["pick_color".to_string()], + }; + + let extension = config_to_goose_extension(&config).expect("conversion should succeed"); + + assert!(extension.is_none()); + } + + #[test] + fn sse_config_is_skipped() { + let config = ExtensionConfig::Sse { + name: "legacy-sse".to_string(), + description: "Legacy SSE".to_string(), + uri: Some("https://example.com/sse".to_string()), + }; + + let extension = config_to_goose_extension(&config).expect("conversion should succeed"); + + assert!(extension.is_none()); + } + + #[test] + fn goose_mcp_stdio_extension_converts_to_config_without_literal_envs() { + let extension = GooseExtension::Mcp { + server: McpServer::Stdio( + McpServerStdio::new("test-stdio", "test-command") + .args(vec!["--flag".to_string(), "value".to_string()]), + ), + env_keys: vec!["SECRET_TOKEN".to_string()], + description: Some("Test stdio".to_string()), + timeout: Some(42), + socket: None, + bundled: Some(true), + }; + + let conversion = goose_extension_to_config(extension).expect("conversion should succeed"); + assert!(conversion.secret_updates.is_empty()); + + let ExtensionConfig::Stdio { + name, + description, + cmd, + args, + envs, + env_keys, + timeout, + bundled, + available_tools, + } = conversion.config + else { + panic!("expected stdio config"); + }; + + assert_eq!(name, "test-stdio"); + assert_eq!(description, "Test stdio"); + assert_eq!(cmd, "test-command"); + assert_eq!(args, vec!["--flag", "value"]); + assert!( + envs.get_env().is_empty(), + "literal envs should not be persisted" + ); + assert_eq!(env_keys, vec!["SECRET_TOKEN"]); + assert_eq!(timeout, Some(42)); + assert_eq!(bundled, Some(true)); + assert!(available_tools.is_empty()); + } + + #[test] + fn goose_mcp_stdio_extension_extracts_literal_envs_for_config_add() { + let extension = GooseExtension::Mcp { + server: McpServer::Stdio(McpServerStdio::new("test-stdio", "test-command").env(vec![ + agent_client_protocol::schema::EnvVariable::new("SECRET_TOKEN", "literal-secret"), + agent_client_protocol::schema::EnvVariable::new("OTHER_TOKEN", "other-secret"), + ])), + env_keys: vec!["SECRET_TOKEN".to_string()], + description: Some("Test stdio".to_string()), + timeout: Some(42), + socket: None, + bundled: Some(true), + }; + + let conversion = goose_extension_to_config(extension).expect("conversion should succeed"); + + assert_eq!( + conversion.secret_updates, + vec![ + ( + "SECRET_TOKEN".to_string(), + serde_json::Value::String("literal-secret".to_string()) + ), + ( + "OTHER_TOKEN".to_string(), + serde_json::Value::String("other-secret".to_string()) + ) + ] + ); + + let ExtensionConfig::Stdio { envs, env_keys, .. } = conversion.config else { + panic!("expected stdio config"); + }; + + assert!( + envs.get_env().is_empty(), + "literal envs should not be persisted" + ); + assert_eq!(env_keys, vec!["SECRET_TOKEN", "OTHER_TOKEN"]); + } + + #[test] + fn goose_mcp_streamable_http_extension_converts_to_config_without_literal_envs() { + let extension = GooseExtension::Mcp { + server: McpServer::Http( + McpServerHttp::new("test-http", "https://example.com/mcp").headers(vec![ + HttpHeader::new("Authorization", "Bearer ${API_TOKEN}"), + ]), + ), + env_keys: vec!["API_TOKEN".to_string()], + description: Some("Test HTTP".to_string()), + timeout: Some(99), + socket: Some("@egress.sock".to_string()), + bundled: Some(true), + }; + + let conversion = goose_extension_to_config(extension).expect("conversion should succeed"); + assert!(conversion.secret_updates.is_empty()); + + let ExtensionConfig::StreamableHttp { + name, + description, + uri, + envs, + env_keys, + headers, + timeout, + socket, + bundled, + available_tools, + } = conversion.config + else { + panic!("expected streamable http config"); + }; + + assert_eq!(name, "test-http"); + assert_eq!(description, "Test HTTP"); + assert_eq!(uri, "https://example.com/mcp"); + assert!( + envs.get_env().is_empty(), + "literal envs should not be persisted" + ); + assert_eq!(env_keys, vec!["API_TOKEN"]); + assert_eq!( + headers, + HashMap::from([( + "Authorization".to_string(), + "Bearer ${API_TOKEN}".to_string() + )]) + ); + assert_eq!(timeout, Some(99)); + assert_eq!(socket.as_deref(), Some("@egress.sock")); + assert_eq!(bundled, Some(true)); + assert!(available_tools.is_empty()); + } + + #[test] + fn goose_builtin_extension_converts_to_config() { + let builtin = GooseExtension::Builtin { + name: "developer".to_string(), + description: Some("Developer tools".to_string()), + display_name: Some("Developer".to_string()), + timeout: Some(30), + bundled: Some(true), + }; + + let conversion = goose_extension_to_config(builtin).expect("conversion should succeed"); + assert!(conversion.secret_updates.is_empty()); + + let ExtensionConfig::Builtin { + name, + description, + display_name, + timeout, + bundled, + available_tools, + } = conversion.config + else { + panic!("expected builtin config"); + }; + + assert_eq!(name, "developer"); + assert_eq!(description, "Developer tools"); + assert_eq!(display_name.as_deref(), Some("Developer")); + assert_eq!(timeout, Some(30)); + assert_eq!(bundled, Some(true)); + assert!(available_tools.is_empty()); + } + + #[test] + fn goose_platform_extension_converts_to_config() { + let platform = GooseExtension::Platform { + name: "todo".to_string(), + description: Some("Todo tools".to_string()), + display_name: Some("Todo".to_string()), + bundled: Some(true), + }; + + let conversion = goose_extension_to_config(platform).expect("conversion should succeed"); + assert!(conversion.secret_updates.is_empty()); + + let ExtensionConfig::Platform { + name, + description, + display_name, + bundled, + available_tools, + } = conversion.config + else { + panic!("expected platform config"); + }; + + assert_eq!(name, "todo"); + assert_eq!(description, "Todo tools"); + assert_eq!(display_name.as_deref(), Some("Todo")); + assert_eq!(bundled, Some(true)); + assert!(available_tools.is_empty()); + } + + #[test] + fn goose_mcp_sse_extension_is_rejected_for_config_add() { + let extension = GooseExtension::Mcp { + server: McpServer::Sse(McpServerSse::new("legacy-sse", "https://example.com/sse")), + env_keys: Vec::new(), + description: None, + timeout: None, + socket: None, + bundled: None, + }; + + assert!(goose_extension_to_config(extension).is_err()); + } +} diff --git a/crates/goose/src/bin/generate_acp_schema.rs b/crates/goose/src/bin/generate_acp_schema.rs index 90daed77..bfbf1f7a 100644 --- a/crates/goose/src/bin/generate_acp_schema.rs +++ b/crates/goose/src/bin/generate_acp_schema.rs @@ -74,6 +74,8 @@ fn main() { strip_integer_formats(def); } + add_mcp_server_transport_discriminants(&mut defs); + // Annotate $defs entries with x-method/x-side. Only set x-method for types // used by exactly one method (shared types like EmptyResponse skip x-method). for (name, methods_list) in &type_methods { @@ -316,6 +318,42 @@ fn rewrite_unstable_schema_refs(value: &mut Value, unstable_type_names: &BTreeSe } } +fn add_mcp_server_transport_discriminants(defs: &mut Map) { + add_object_discriminant(defs, "McpServerHttp", "http"); + add_object_discriminant(defs, "McpServerSse", "sse"); +} + +fn add_object_discriminant(defs: &mut Map, def_name: &str, tag: &str) { + let def = defs + .get_mut(def_name) + .unwrap_or_else(|| panic!("missing {def_name} schema definition")); + let obj = def + .as_object_mut() + .unwrap_or_else(|| panic!("{def_name} schema definition must be an object")); + + let properties = obj + .entry("properties") + .or_insert_with(|| json!({})) + .as_object_mut() + .unwrap_or_else(|| panic!("{def_name}.properties must be an object")); + properties.insert( + "type".into(), + json!({ + "type": "string", + "const": tag, + }), + ); + + let required = obj + .entry("required") + .or_insert_with(|| json!([])) + .as_array_mut() + .unwrap_or_else(|| panic!("{def_name}.required must be an array")); + if !required.iter().any(|item| item.as_str() == Some("type")) { + required.insert(0, json!("type")); + } +} + /// Recursively strip `"format"` from integer-typed schemas. /// /// schemars emits `"format": "uint64"` / `"int64"` etc. for Rust integer types. @@ -323,7 +361,15 @@ fn rewrite_unstable_schema_refs(value: &mut Value, unstable_type_names: &BTreeSe fn strip_integer_formats(value: &mut Value) { match value { Value::Object(map) => { - let is_integer = map.get("type").and_then(|v| v.as_str()) == Some("integer"); + let is_integer = match map.get("type") { + Some(Value::String(schema_type)) => schema_type == "integer", + Some(Value::Array(schema_types)) => schema_types.iter().any(|schema_type| { + schema_type + .as_str() + .is_some_and(|schema_type| schema_type == "integer") + }), + _ => false, + }; if is_integer { map.remove("format"); } @@ -367,3 +413,105 @@ fn replace_true_schemas(value: &mut Value) { _ => {} } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn adds_http_and_sse_discriminants_without_tagging_stdio() { + let mut defs = Map::from_iter([ + ( + "McpServerHttp".into(), + json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + }), + ), + ( + "McpServerSse".into(), + json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + }), + ), + ( + "McpServerStdio".into(), + json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + }, + "required": ["name"] + }), + ), + ]); + + add_mcp_server_transport_discriminants(&mut defs); + + assert_eq!( + defs["McpServerHttp"]["properties"]["type"], + json!({ "type": "string", "const": "http" }) + ); + assert_eq!( + defs["McpServerSse"]["properties"]["type"], + json!({ "type": "string", "const": "sse" }) + ); + assert_eq!(defs["McpServerStdio"]["properties"].get("type"), None); + assert!(defs["McpServerHttp"]["required"] + .as_array() + .unwrap() + .contains(&json!("type"))); + assert!(defs["McpServerSse"]["required"] + .as_array() + .unwrap() + .contains(&json!("type"))); + } + + #[test] + fn strips_integer_formats_from_nullable_integer_schemas() { + let mut schema = json!({ + "type": "object", + "properties": { + "timeout": { + "type": ["integer", "null"], + "format": "uint64", + "minimum": 0 + }, + "count": { + "type": "integer", + "format": "uint32", + "minimum": 0 + }, + "name": { + "type": "string", + "format": "custom" + } + } + }); + + strip_integer_formats(&mut schema); + + assert_eq!( + schema["properties"]["timeout"].get("format"), + None, + "nullable integer formats should be stripped" + ); + assert_eq!( + schema["properties"]["count"].get("format"), + None, + "integer formats should be stripped" + ); + assert_eq!( + schema["properties"]["name"]["format"], + json!("custom"), + "non-integer formats should be preserved" + ); + } +} diff --git a/crates/goose/src/builtin_extension.rs b/crates/goose/src/builtin_extension.rs index c69a44cf..5dfa5984 100644 --- a/crates/goose/src/builtin_extension.rs +++ b/crates/goose/src/builtin_extension.rs @@ -22,3 +22,7 @@ pub fn register_builtin_extensions(extensions: HashMap<&'static str, SpawnServer pub fn get_builtin_extension(name: &str) -> Option { BUILTIN_REGISTRY.read().unwrap().get(name).cloned() } + +pub fn get_builtin_extension_names() -> Vec<&'static str> { + BUILTIN_REGISTRY.read().unwrap().keys().copied().collect() +} diff --git a/crates/goose/src/config/extensions.rs b/crates/goose/src/config/extensions.rs index 460a6e93..237ee8b6 100644 --- a/crates/goose/src/config/extensions.rs +++ b/crates/goose/src/config/extensions.rs @@ -106,12 +106,16 @@ pub fn remove_extension(key: &str) { save_extensions_map(extensions); } -pub fn set_extension_enabled(key: &str, enabled: bool) { +/// Returns true when an existing extension was updated, false when the key was missing. +pub fn set_extension_enabled(key: &str, enabled: bool) -> bool { let mut extensions = get_extensions_map(); - if let Some(entry) = extensions.get_mut(key) { - entry.enabled = enabled; - save_extensions_map(extensions); - } + let Some(entry) = extensions.get_mut(key) else { + return false; + }; + + entry.enabled = enabled; + save_extensions_map(extensions); + true } pub fn get_all_extensions() -> Vec { @@ -145,6 +149,40 @@ pub fn get_enabled_extensions_with_config(config: &Config) -> Vec Vec { + let mut builtin_names = crate::builtin_extension::get_builtin_extension_names(); + builtin_names.sort_unstable(); + + let mut platform_definitions = PLATFORM_EXTENSIONS + .values() + .filter(|definition| !definition.hidden) + .collect::>(); + platform_definitions.sort_unstable_by_key(|definition| definition.name); + + builtin_names + .into_iter() + .map(|name| ExtensionConfig::Builtin { + name: name.to_string(), + description: String::new(), + display_name: Some(name.to_string()), + timeout: None, + bundled: Some(true), + available_tools: Vec::new(), + }) + .chain( + platform_definitions + .into_iter() + .map(|definition| ExtensionConfig::Platform { + name: definition.name.to_string(), + description: definition.description.to_string(), + display_name: Some(definition.display_name.to_string()), + bundled: Some(true), + available_tools: Vec::new(), + }), + ) + .collect() +} + pub fn get_warnings() -> Vec { let raw: Mapping = Config::global() .get_param(EXTENSIONS_CONFIG_KEY) diff --git a/crates/goose/src/config/mod.rs b/crates/goose/src/config/mod.rs index 402c835f..ae6c4626 100644 --- a/crates/goose/src/config/mod.rs +++ b/crates/goose/src/config/mod.rs @@ -17,9 +17,9 @@ pub use base::{merge_config_values, Config, ConfigError}; 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, - get_warnings, is_extension_enabled, remove_extension, resolve_extensions_for_new_session, - set_extension, set_extension_enabled, ExtensionEntry, + get_all_extension_names, get_all_extensions, get_available_extensions, get_enabled_extensions, + get_extension_by_name, get_warnings, is_extension_enabled, remove_extension, + resolve_extensions_for_new_session, set_extension, set_extension_enabled, ExtensionEntry, }; pub use goose_mode::GooseMode; pub use permission::PermissionManager; diff --git a/crates/goose/tests/acp_custom_requests_test.rs b/crates/goose/tests/acp_custom_requests_test.rs index 659a820e..9db97bc8 100644 --- a/crates/goose/tests/acp_custom_requests_test.rs +++ b/crates/goose/tests/acp_custom_requests_test.rs @@ -18,15 +18,21 @@ use std::sync::{Arc, LazyLock, Mutex}; use common_tests::fixtures::OpenAiFixture; -const DEFAULT_ACP_TEST_CONFIG: &str = "GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\n"; +const DEFAULT_ACP_TEST_CONFIG: &str = + "GOOSE_MODEL: gpt-4o\nGOOSE_PROVIDER: openai\nGOOSE_DISABLE_KEYRING: true\n"; static ACP_CONFIG_ROOT: LazyLock = LazyLock::new(|| tempfile::tempdir().unwrap()); fn write_acp_global_config(contents: &str) -> PathBuf { std::env::set_var("GOOSE_PATH_ROOT", ACP_CONFIG_ROOT.path()); + std::env::set_var("GOOSE_DISABLE_KEYRING", "1"); let config_dir = goose::config::paths::Paths::config_dir(); std::fs::create_dir_all(&config_dir).unwrap(); + let mut contents = contents.to_string(); + if !contents.contains("GOOSE_DISABLE_KEYRING") { + contents.push_str("GOOSE_DISABLE_KEYRING: true\n"); + } std::fs::write( config_dir.join(goose::config::base::CONFIG_YAML_NAME), contents, @@ -120,6 +126,132 @@ fn test_custom_get_tools() { #[test] #[serial] fn test_custom_get_extensions() { + let config_key = "test-stdio-acp-mutation-flow"; + let _guard = env_lock::lock_env([("EXTENSIONS", None::<&str>)]); + write_acp_global_config(DEFAULT_ACP_TEST_CONFIG); + + run_test(async move { + let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; + let conn = AcpServerConnection::new(TestConnectionConfig::default(), openai).await; + + let add_result = send_custom( + conn.cx(), + "_goose/unstable/config/extensions/add", + serde_json::json!({ + "enabled": true, + "extension": { + "type": "mcp", + "description": "Test stdio", + "envKeys": ["SECRET_TOKEN"], + "timeout": 42, + "server": { + "type": "stdio", + "name": config_key, + "command": "test-command", + "args": ["--flag", "value"], + "env": [ + { "name": "INLINE_TOKEN", "value": "inline-secret" } + ] + } + } + }), + ) + .await; + assert!(add_result.is_ok(), "expected ok, got: {:?}", add_result); + let stored_inline_token = goose::config::Config::global() + .get_secret::("INLINE_TOKEN") + .expect("inline env should be saved as a secret"); + assert!( + stored_inline_token == "inline-secret", + "inline env secret was not saved correctly" + ); + + let list_extension = || async { + let result = send_custom( + conn.cx(), + "_goose/unstable/config/extensions/list", + serde_json::json!({}), + ) + .await; + assert!(result.is_ok(), "expected ok, got: {:?}", result); + + let response = result.unwrap(); + let extensions = response + .get("extensions") + .and_then(|extensions| extensions.as_array()) + .expect("extensions should be an array"); + extensions + .iter() + .find(|entry| entry["configKey"] == config_key) + .cloned() + }; + + let entry = list_extension() + .await + .unwrap_or_else(|| panic!("missing added extension entry")); + assert_eq!(entry["enabled"], true); + assert_eq!(entry["configKey"], config_key); + + let extension = &entry["extension"]; + assert_eq!(extension["type"], "mcp"); + assert_eq!( + extension["envKeys"], + serde_json::json!(["SECRET_TOKEN", "INLINE_TOKEN"]) + ); + assert_eq!(extension["description"], "Test stdio"); + assert_eq!(extension["timeout"], 42); + assert!(extension.get("socket").is_none()); + + let server = &extension["server"]; + assert_eq!(server["name"], config_key); + assert_eq!(server["command"], "test-command"); + assert_eq!(server["args"], serde_json::json!(["--flag", "value"])); + assert_eq!(server["env"], serde_json::json!([])); + + let set_enabled_result = send_custom( + conn.cx(), + "_goose/unstable/config/extensions/set-enabled", + serde_json::json!({ + "configKey": config_key, + "enabled": false, + }), + ) + .await; + assert!( + set_enabled_result.is_ok(), + "expected ok, got: {:?}", + set_enabled_result + ); + + let entry = list_extension() + .await + .unwrap_or_else(|| panic!("missing disabled extension entry")); + assert_eq!(entry["enabled"], false); + + let remove_result = send_custom( + conn.cx(), + "_goose/unstable/config/extensions/remove", + serde_json::json!({ + "configKey": config_key, + }), + ) + .await; + assert!( + remove_result.is_ok(), + "expected ok, got: {:?}", + remove_result + ); + + assert!( + list_extension().await.is_none(), + "removed extension should not be listed" + ); + }); +} + +#[test] +#[serial] +fn test_custom_get_available_extensions() { write_acp_global_config(DEFAULT_ACP_TEST_CONFIG); run_test(async move { let openai = OpenAiFixture::new(vec![], Arc::new(EnforceSessionId::default())).await; @@ -127,20 +259,36 @@ fn test_custom_get_extensions() { let result = send_custom( conn.cx(), - "_goose/unstable/config/extensions/list", + "_goose/unstable/extensions/available", serde_json::json!({}), ) .await; assert!(result.is_ok(), "expected ok, got: {:?}", result); let response = result.unwrap(); + let extensions = response + .get("extensions") + .and_then(|extensions| extensions.as_array()) + .expect("extensions should be an array"); + assert!(!extensions.is_empty(), "extensions should not be empty"); assert!( - response.get("extensions").is_some(), - "missing 'extensions' field" + extensions.iter().all(|extension| matches!( + extension["type"].as_str(), + Some("builtin" | "platform") + )), + "available extensions should only include builtin and platform entries" ); assert!( - response.get("warnings").is_some(), - "missing 'warnings' field" + extensions.iter().any(|extension| { + extension["type"] == "platform" && extension["name"] == "developer" + }), + "developer platform extension should be available" + ); + assert!( + !extensions.iter().any(|extension| { + extension["type"] == "platform" && extension["name"] == "orchestrator" + }), + "hidden orchestrator platform extension should not be available" ); }); } diff --git a/ui/desktop/src/acp/extensions.ts b/ui/desktop/src/acp/extensions.ts index 4edcff17..7964210b 100644 --- a/ui/desktop/src/acp/extensions.ts +++ b/ui/desktop/src/acp/extensions.ts @@ -1,11 +1,77 @@ import type { ExtensionResponse, ExtensionEntry } from '../api'; +import type { GooseExtensionEntry, McpServer } from '@aaif/goose-sdk'; import { getAcpClient } from './acpConnection'; +function headersToRecord(headers: { name: string; value: string }[] = []) { + return Object.fromEntries(headers.map(({ name, value }) => [name, value])); +} + +function mcpServerToExtension( + server: McpServer, + entry: GooseExtensionEntry +): ExtensionEntry | null { + const extension = entry.extension; + if (extension.type !== 'mcp') { + return null; + } + + if ('command' in server) { + return { + type: 'stdio', + enabled: entry.enabled, + name: server.name, + description: extension.description ?? '', + cmd: server.command, + args: server.args, + env_keys: extension.envKeys ?? [], + timeout: extension.timeout, + bundled: extension.bundled, + }; + } + + if ('url' in server) { + return { + type: 'streamable_http', + enabled: entry.enabled, + name: server.name, + description: extension.description ?? '', + uri: server.url, + headers: headersToRecord(server.headers), + env_keys: extension.envKeys ?? [], + timeout: extension.timeout, + socket: extension.socket, + bundled: extension.bundled, + }; + } + + return null; +} + +function gooseExtensionEntryToExtensionEntry(entry: GooseExtensionEntry): ExtensionEntry | null { + const extension = entry.extension; + + switch (extension.type) { + case 'builtin': + case 'platform': + return { + ...extension, + description: extension.description ?? '', + enabled: entry.enabled, + }; + case 'mcp': + return mcpServerToExtension(extension.server, entry); + } + + return null; +} + export async function getConfiguredExtensions(): Promise { const client = await getAcpClient(); const response = await client.goose.configExtensionsList_unstable({}); return { - extensions: response.extensions as ExtensionEntry[], - warnings: response.warnings, + extensions: response.extensions + .map(gooseExtensionEntryToExtensionEntry) + .filter((entry): entry is ExtensionEntry => entry !== null), + warnings: response.warnings ?? [], }; } diff --git a/ui/sdk/src/generated/client.gen.ts b/ui/sdk/src/generated/client.gen.ts index 9467dd0d..a11a411c 100644 --- a/ui/sdk/src/generated/client.gen.ts +++ b/ui/sdk/src/generated/client.gen.ts @@ -46,8 +46,10 @@ import type { ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, - GetExtensionsRequest_unstable, - GetExtensionsResponse_unstable, + GetAvailableExtensionsRequest_unstable, + GetAvailableExtensionsResponse_unstable, + GetConfigExtensionsRequest_unstable, + GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetToolsRequest_unstable, @@ -94,8 +96,8 @@ import type { RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, + SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, - ToggleConfigExtensionRequest_unstable, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, @@ -115,7 +117,8 @@ import { zDictationTranscribeResponse_unstable, zExportSessionResponse_unstable, zExportSourceResponse_unstable, - zGetExtensionsResponse_unstable, + zGetAvailableExtensionsResponse_unstable, + zGetConfigExtensionsResponse_unstable, zGetSessionExtensionsResponse_unstable, zGetToolsResponse_unstable, zGooseSessionNotification_unstable, @@ -208,15 +211,27 @@ export class GooseExtClient { } async configExtensionsList_unstable( - params: GetExtensionsRequest_unstable, - ): Promise { + params: GetConfigExtensionsRequest_unstable, + ): Promise { const raw = await this.conn.extMethod( "_goose/unstable/config/extensions/list", params, ); - return zGetExtensionsResponse_unstable.parse( + return zGetConfigExtensionsResponse_unstable.parse( raw, - ) as GetExtensionsResponse_unstable; + ) as GetConfigExtensionsResponse_unstable; + } + + async extensionsAvailable_unstable( + params: GetAvailableExtensionsRequest_unstable, + ): Promise { + const raw = await this.conn.extMethod( + "_goose/unstable/extensions/available", + params, + ); + return zGetAvailableExtensionsResponse_unstable.parse( + raw, + ) as GetAvailableExtensionsResponse_unstable; } async configExtensionsAdd_unstable( @@ -234,11 +249,11 @@ export class GooseExtClient { ); } - async configExtensionsToggle_unstable( - params: ToggleConfigExtensionRequest_unstable, + async configExtensionsSetEnabled_unstable( + params: SetConfigExtensionEnabledRequest_unstable, ): Promise { await this.conn.extMethod( - "_goose/unstable/config/extensions/toggle", + "_goose/unstable/config/extensions/set-enabled", params, ); } diff --git a/ui/sdk/src/generated/index.ts b/ui/sdk/src/generated/index.ts index 6e348832..ea3d436b 100644 --- a/ui/sdk/src/generated/index.ts +++ b/ui/sdk/src/generated/index.ts @@ -1,6 +1,6 @@ // This file is auto-generated by @hey-api/openapi-ts -export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, ArchiveSessionRequest_unstable, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, ElicitationRespondRequest_unstable, EmptyResponse, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetExtensionsRequest_unstable, GetExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, Interaction, InteractionState, InteractionUpdate, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, SessionSystemPromptMode, SessionUsageUpdate, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, ToggleConfigExtensionRequest_unstable, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; +export type { AddConfigExtensionRequest_unstable, AddExtensionRequest_unstable, ArchiveSessionRequest_unstable, CreateSourceRequest_unstable, CreateSourceResponse_unstable, CustomProviderConfigDto, CustomProviderCreateRequest_unstable, CustomProviderCreateResponse_unstable, CustomProviderDeleteRequest_unstable, CustomProviderDeleteResponse_unstable, CustomProviderReadRequest_unstable, CustomProviderReadResponse_unstable, CustomProviderUpdateRequest_unstable, CustomProviderUpdateResponse_unstable, DefaultsReadRequest_unstable, DefaultsReadResponse_unstable, DefaultsSaveRequest_unstable, DeleteSessionRequest, DeleteSourceRequest_unstable, DictationConfigRequest_unstable, DictationConfigResponse_unstable, DictationDownloadProgress, DictationLocalModelStatus, DictationModelCancelRequest_unstable, DictationModelDeleteRequest_unstable, DictationModelDownloadProgressRequest_unstable, DictationModelDownloadProgressResponse_unstable, DictationModelDownloadRequest_unstable, DictationModelOption, DictationModelSelectRequest_unstable, DictationModelsListRequest_unstable, DictationModelsListResponse_unstable, DictationProviderStatusEntry, DictationSecretDeleteRequest_unstable, DictationSecretSaveRequest_unstable, DictationTranscribeRequest_unstable, DictationTranscribeResponse_unstable, ElicitationRespondRequest_unstable, EmptyResponse, EnvVariable, ExportSessionRequest_unstable, ExportSessionResponse_unstable, ExportSourceRequest_unstable, ExportSourceResponse_unstable, ExtNotification, ExtRequest, ExtResponse, GetAvailableExtensionsRequest_unstable, GetAvailableExtensionsResponse_unstable, GetConfigExtensionsRequest_unstable, GetConfigExtensionsResponse_unstable, GetSessionExtensionsRequest_unstable, GetSessionExtensionsResponse_unstable, GetToolsRequest_unstable, GetToolsResponse_unstable, GooseExtension, GooseExtensionEntry, GooseSessionNotification_unstable, GooseSessionUpdate, GooseToolCallRequest_unstable, GooseToolCallResponse_unstable, HttpHeader, ImportSessionRequest_unstable, ImportSessionResponse_unstable, ImportSourcesRequest_unstable, ImportSourcesResponse_unstable, Interaction, InteractionState, InteractionUpdate, ListProvidersRequest_unstable, ListProvidersResponse_unstable, ListSourcesRequest_unstable, ListSourcesResponse_unstable, McpServer, McpServerHttp, McpServerSse, McpServerStdio, OnboardingImportApplyRequest_unstable, OnboardingImportApplyResponse_unstable, OnboardingImportCandidate, OnboardingImportCounts, OnboardingImportScanRequest_unstable, OnboardingImportScanResponse_unstable, OnboardingImportSourceKind, PreferenceKey, PreferencesReadRequest_unstable, PreferencesReadResponse_unstable, PreferencesRemoveRequest_unstable, PreferencesSaveRequest_unstable, PreferenceValue, ProviderCatalogListRequest_unstable, ProviderCatalogListResponse_unstable, ProviderCatalogTemplateRequest_unstable, ProviderCatalogTemplateResponse_unstable, ProviderConfigAuthenticateRequest_unstable, ProviderConfigChangeResponse_unstable, ProviderConfigDeleteRequest_unstable, ProviderConfigFieldUpdate, ProviderConfigFieldValueDto, ProviderConfigKey, ProviderConfigReadRequest_unstable, ProviderConfigReadResponse_unstable, ProviderConfigSaveRequest_unstable, ProviderConfigStatusDto, ProviderConfigStatusRequest_unstable, ProviderConfigStatusResponse_unstable, ProviderInventoryEntryDto, ProviderInventoryModelDto, ProviderSetupCatalogEntryDto, ProviderSetupCatalogListRequest_unstable, ProviderSetupCatalogListResponse_unstable, ProviderSetupCategoryDto, ProviderSetupFieldDto, ProviderSetupGroupDto, ProviderSetupMethodDto, ProviderSupportedModelsListRequest_unstable, ProviderSupportedModelsListResponse_unstable, ProviderTemplateCapabilitiesDto, ProviderTemplateCatalogEntryDto, ProviderTemplateDto, ProviderTemplateModelDto, ReadResourceRequest_unstable, ReadResourceResponse_unstable, RefreshProviderInventoryRequest_unstable, RefreshProviderInventoryResponse_unstable, RefreshProviderInventorySkipDto, RefreshProviderInventorySkipReasonDto, RemoveConfigExtensionRequest_unstable, RemoveExtensionRequest_unstable, RenameSessionRequest_unstable, SessionSystemPromptMode, SessionUsageUpdate, SetConfigExtensionEnabledRequest_unstable, SetSessionSystemPromptRequest_unstable, SourceEntry, SourceScope, SourceType, StatusMessage, StatusMessageUpdate, UnarchiveSessionRequest_unstable, UpdateSessionProjectRequest_unstable, UpdateSourceRequest_unstable, UpdateSourceResponse_unstable, UpdateWorkingDirRequest_unstable } from './types.gen.js'; export const GOOSE_EXT_METHODS = [ { @@ -45,8 +45,13 @@ export const GOOSE_EXT_METHODS = [ }, { method: "_goose/unstable/config/extensions/list", - requestType: "GetExtensionsRequest_unstable", - responseType: "GetExtensionsResponse_unstable", + requestType: "GetConfigExtensionsRequest_unstable", + responseType: "GetConfigExtensionsResponse_unstable", + }, + { + method: "_goose/unstable/extensions/available", + requestType: "GetAvailableExtensionsRequest_unstable", + responseType: "GetAvailableExtensionsResponse_unstable", }, { method: "_goose/unstable/config/extensions/add", @@ -59,8 +64,8 @@ export const GOOSE_EXT_METHODS = [ responseType: "EmptyResponse", }, { - method: "_goose/unstable/config/extensions/toggle", - requestType: "ToggleConfigExtensionRequest_unstable", + method: "_goose/unstable/config/extensions/set-enabled", + requestType: "SetConfigExtensionEnabledRequest_unstable", responseType: "EmptyResponse", }, { diff --git a/ui/sdk/src/generated/types.gen.ts b/ui/sdk/src/generated/types.gen.ts index cfec115f..bf7bf311 100644 --- a/ui/sdk/src/generated/types.gen.ts +++ b/ui/sdk/src/generated/types.gen.ts @@ -119,32 +119,211 @@ export type DeleteSessionRequest = { /** * List configured extensions and any warnings. */ -export type GetExtensionsRequest_unstable = { +export type GetConfigExtensionsRequest_unstable = { [key: string]: unknown; }; /** * List configured extensions and any warnings. */ -export type GetExtensionsResponse_unstable = { +export type GetConfigExtensionsResponse_unstable = { + extensions: Array; + warnings?: Array; +}; + +export type GooseExtensionEntry = { + extension: GooseExtension; + enabled: boolean; + configKey?: string | null; +}; + +export type GooseExtension = { + name: string; + description?: string | null; + display_name?: string | null; + timeout?: number | null; + bundled?: boolean | null; + type: 'builtin'; +} | { + name: string; + description?: string | null; + display_name?: string | null; + bundled?: boolean | null; + type: 'platform'; +} | { + server: McpServer; + envKeys?: Array; + description?: string | null; + timeout?: number | null; + socket?: string | null; + bundled?: boolean | null; + type: 'mcp'; +}; + +/** + * Configuration for connecting to an MCP (Model Context Protocol) server. + * + * MCP servers provide tools and context that the agent can use when + * processing prompts. + * + * See protocol docs: [MCP Servers](https://agentclientprotocol.com/protocol/session-setup#mcp-servers) + */ +export type McpServer = McpServerHttp | McpServerSse | McpServerStdio; + +/** + * An HTTP header to set when making requests to the MCP server. + */ +export type HttpHeader = { /** - * Array of ExtensionEntry objects with `enabled` flag, `configKey`, and flattened config details. + * The name of the HTTP header. */ - extensions: Array; - warnings: Array; + name: string; + /** + * The value to set for the HTTP header. + */ + value: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * HTTP transport configuration for MCP. + */ +export type McpServerHttp = { + /** + * Human-readable name identifying this MCP server. + */ + name: string; + /** + * URL to the MCP server. + */ + url: string; + /** + * HTTP headers to set when making requests to the MCP server. + */ + headers: Array; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + type: 'http'; +}; + +/** + * SSE transport configuration for MCP. + */ +export type McpServerSse = { + /** + * Human-readable name identifying this MCP server. + */ + name: string; + /** + * URL to the MCP server. + */ + url: string; + /** + * HTTP headers to set when making requests to the MCP server. + */ + headers: Array; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; + type: 'sse'; +}; + +/** + * Stdio transport configuration for MCP. + */ +export type McpServerStdio = { + /** + * Human-readable name identifying this MCP server. + */ + name: string; + /** + * Path to the MCP server executable. + */ + command: string; + /** + * Command-line arguments to pass to the MCP server. + */ + args: Array; + /** + * Environment variables to set when launching the MCP server. + */ + env: Array; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * An environment variable to set when launching an MCP server. + */ +export type EnvVariable = { + /** + * The name of the environment variable. + */ + name: string; + /** + * The value to set for the environment variable. + */ + value: string; + /** + * The _meta property is reserved by ACP to allow clients and agents to attach additional + * metadata to their interactions. Implementations MUST NOT make assumptions about values at + * these keys. + * + * See protocol docs: [Extensibility](https://agentclientprotocol.com/protocol/extensibility) + */ + _meta?: { + [key: string]: unknown; + } | null; +}; + +/** + * List Goose-owned extension definitions available to configure or enable. + */ +export type GetAvailableExtensionsRequest_unstable = { + [key: string]: unknown; +}; + +export type GetAvailableExtensionsResponse_unstable = { + extensions: Array; }; /** * Persist a new extension to the user's global goose config. */ export type AddConfigExtensionRequest_unstable = { - name: string; - /** - * Extension configuration. Must be a JSON object matching one of the - * `ExtensionConfig` variants (e.g. `stdio`, `streamable_http`, `builtin`). - * `name` and `enabled` are injected server-side. - */ - extensionConfig?: unknown; + extension: GooseExtension; enabled?: boolean; }; @@ -156,9 +335,9 @@ export type RemoveConfigExtensionRequest_unstable = { }; /** - * Toggle the `enabled` flag for a persisted extension in the user's global goose config. + * Set the `enabled` flag for a persisted extension in the user's global goose config. */ -export type ToggleConfigExtensionRequest_unstable = { +export type SetConfigExtensionEnabledRequest_unstable = { configKey: string; enabled: boolean; }; @@ -1166,14 +1345,14 @@ export type InteractionUpdate = { export type ExtRequest = { id: string; method: string; - params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | DeleteSessionRequest | GetExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | ToggleConfigExtensionRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | ElicitationRespondRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { + params?: AddExtensionRequest_unstable | RemoveExtensionRequest_unstable | GetToolsRequest_unstable | GooseToolCallRequest_unstable | ReadResourceRequest_unstable | UpdateWorkingDirRequest_unstable | SetSessionSystemPromptRequest_unstable | DeleteSessionRequest | GetConfigExtensionsRequest_unstable | GetAvailableExtensionsRequest_unstable | AddConfigExtensionRequest_unstable | RemoveConfigExtensionRequest_unstable | SetConfigExtensionEnabledRequest_unstable | GetSessionExtensionsRequest_unstable | ListProvidersRequest_unstable | ProviderSupportedModelsListRequest_unstable | ProviderCatalogListRequest_unstable | ProviderSetupCatalogListRequest_unstable | ProviderCatalogTemplateRequest_unstable | CustomProviderCreateRequest_unstable | CustomProviderReadRequest_unstable | CustomProviderUpdateRequest_unstable | CustomProviderDeleteRequest_unstable | RefreshProviderInventoryRequest_unstable | ProviderConfigReadRequest_unstable | ProviderConfigStatusRequest_unstable | ProviderConfigSaveRequest_unstable | ProviderConfigDeleteRequest_unstable | ProviderConfigAuthenticateRequest_unstable | PreferencesReadRequest_unstable | PreferencesSaveRequest_unstable | PreferencesRemoveRequest_unstable | DefaultsReadRequest_unstable | DefaultsSaveRequest_unstable | OnboardingImportScanRequest_unstable | OnboardingImportApplyRequest_unstable | ExportSessionRequest_unstable | ImportSessionRequest_unstable | ElicitationRespondRequest_unstable | UpdateSessionProjectRequest_unstable | RenameSessionRequest_unstable | ArchiveSessionRequest_unstable | UnarchiveSessionRequest_unstable | CreateSourceRequest_unstable | ListSourcesRequest_unstable | UpdateSourceRequest_unstable | DeleteSourceRequest_unstable | ExportSourceRequest_unstable | ImportSourcesRequest_unstable | DictationTranscribeRequest_unstable | DictationConfigRequest_unstable | DictationSecretSaveRequest_unstable | DictationSecretDeleteRequest_unstable | DictationModelsListRequest_unstable | DictationModelDownloadRequest_unstable | DictationModelDownloadProgressRequest_unstable | DictationModelCancelRequest_unstable | DictationModelDeleteRequest_unstable | DictationModelSelectRequest_unstable | { [key: string]: unknown; } | null; }; export type ExtResponse = { id: string; - result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | GetExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown; + result?: EmptyResponse | GetToolsResponse_unstable | GooseToolCallResponse_unstable | ReadResourceResponse_unstable | GetConfigExtensionsResponse_unstable | GetAvailableExtensionsResponse_unstable | GetSessionExtensionsResponse_unstable | ListProvidersResponse_unstable | ProviderSupportedModelsListResponse_unstable | ProviderCatalogListResponse_unstable | ProviderSetupCatalogListResponse_unstable | ProviderCatalogTemplateResponse_unstable | CustomProviderCreateResponse_unstable | CustomProviderReadResponse_unstable | CustomProviderUpdateResponse_unstable | CustomProviderDeleteResponse_unstable | RefreshProviderInventoryResponse_unstable | ProviderConfigReadResponse_unstable | ProviderConfigStatusResponse_unstable | ProviderConfigChangeResponse_unstable | PreferencesReadResponse_unstable | DefaultsReadResponse_unstable | OnboardingImportScanResponse_unstable | OnboardingImportApplyResponse_unstable | ExportSessionResponse_unstable | ImportSessionResponse_unstable | CreateSourceResponse_unstable | ListSourcesResponse_unstable | UpdateSourceResponse_unstable | ExportSourceResponse_unstable | ImportSourcesResponse_unstable | DictationTranscribeResponse_unstable | DictationConfigResponse_unstable | DictationModelsListResponse_unstable | DictationModelDownloadProgressResponse_unstable | unknown; } | { error: { code: number; diff --git a/ui/sdk/src/generated/zod.gen.ts b/ui/sdk/src/generated/zod.gen.ts index 0597c7f7..a73387f5 100644 --- a/ui/sdk/src/generated/zod.gen.ts +++ b/ui/sdk/src/generated/zod.gen.ts @@ -115,22 +115,179 @@ export const zDeleteSessionRequest = z.object({ /** * List configured extensions and any warnings. */ -export const zGetExtensionsRequest_unstable = z.record(z.unknown()); +export const zGetConfigExtensionsRequest_unstable = z.record(z.unknown()); + +/** + * An HTTP header to set when making requests to the MCP server. + */ +export const zHttpHeader = z.object({ + name: z.string(), + value: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * HTTP transport configuration for MCP. + */ +export const zMcpServerHttp = z.object({ + name: z.string(), + url: z.string(), + headers: z.array(zHttpHeader), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional(), + type: z.literal('http') +}); + +/** + * SSE transport configuration for MCP. + */ +export const zMcpServerSse = z.object({ + name: z.string(), + url: z.string(), + headers: z.array(zHttpHeader), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional(), + type: z.literal('sse') +}); + +/** + * An environment variable to set when launching an MCP server. + */ +export const zEnvVariable = z.object({ + name: z.string(), + value: z.string(), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Stdio transport configuration for MCP. + */ +export const zMcpServerStdio = z.object({ + name: z.string(), + command: z.string(), + args: z.array(z.string()), + env: z.array(zEnvVariable), + _meta: z.union([ + z.record(z.unknown()), + z.null() + ]).optional() +}); + +/** + * Configuration for connecting to an MCP (Model Context Protocol) server. + * + * MCP servers provide tools and context that the agent can use when + * processing prompts. + * + * See protocol docs: [MCP Servers](https://agentclientprotocol.com/protocol/session-setup#mcp-servers) + */ +export const zMcpServer = z.union([ + zMcpServerHttp, + zMcpServerSse, + zMcpServerStdio +]); + +export const zGooseExtension = z.union([ + z.object({ + name: z.string(), + description: z.union([ + z.string(), + z.null() + ]).optional(), + display_name: z.union([ + z.string(), + z.null() + ]).optional(), + timeout: z.union([ + z.number().int().gte(0), + z.null() + ]).optional(), + bundled: z.union([ + z.boolean(), + z.null() + ]).optional(), + type: z.literal('builtin') + }), + z.object({ + name: z.string(), + description: z.union([ + z.string(), + z.null() + ]).optional(), + display_name: z.union([ + z.string(), + z.null() + ]).optional(), + bundled: z.union([ + z.boolean(), + z.null() + ]).optional(), + type: z.literal('platform') + }), + z.object({ + server: zMcpServer, + envKeys: z.array(z.string()).optional(), + description: z.union([ + z.string(), + z.null() + ]).optional(), + timeout: z.union([ + z.number().int().gte(0), + z.null() + ]).optional(), + socket: z.union([ + z.string(), + z.null() + ]).optional(), + bundled: z.union([ + z.boolean(), + z.null() + ]).optional(), + type: z.literal('mcp') + }) +]); + +export const zGooseExtensionEntry = z.object({ + extension: zGooseExtension, + enabled: z.boolean(), + configKey: z.union([ + z.string(), + z.null() + ]).optional() +}); /** * List configured extensions and any warnings. */ -export const zGetExtensionsResponse_unstable = z.object({ - extensions: z.array(z.unknown()), - warnings: z.array(z.string()) +export const zGetConfigExtensionsResponse_unstable = z.object({ + extensions: z.array(zGooseExtensionEntry), + warnings: z.array(z.string()).optional().default([]) +}); + +/** + * List Goose-owned extension definitions available to configure or enable. + */ +export const zGetAvailableExtensionsRequest_unstable = z.record(z.unknown()); + +export const zGetAvailableExtensionsResponse_unstable = z.object({ + extensions: z.array(zGooseExtension) }); /** * Persist a new extension to the user's global goose config. */ export const zAddConfigExtensionRequest_unstable = z.object({ - name: z.string(), - extensionConfig: z.unknown().optional().default(null), + extension: zGooseExtension, enabled: z.boolean().optional().default(false) }); @@ -142,9 +299,9 @@ export const zRemoveConfigExtensionRequest_unstable = z.object({ }); /** - * Toggle the `enabled` flag for a persisted extension in the user's global goose config. + * Set the `enabled` flag for a persisted extension in the user's global goose config. */ -export const zToggleConfigExtensionRequest_unstable = z.object({ +export const zSetConfigExtensionEnabledRequest_unstable = z.object({ configKey: z.string(), enabled: z.boolean() }); @@ -1190,10 +1347,11 @@ export const zExtRequest = z.object({ zUpdateWorkingDirRequest_unstable, zSetSessionSystemPromptRequest_unstable, zDeleteSessionRequest, - zGetExtensionsRequest_unstable, + zGetConfigExtensionsRequest_unstable, + zGetAvailableExtensionsRequest_unstable, zAddConfigExtensionRequest_unstable, zRemoveConfigExtensionRequest_unstable, - zToggleConfigExtensionRequest_unstable, + zSetConfigExtensionEnabledRequest_unstable, zGetSessionExtensionsRequest_unstable, zListProvidersRequest_unstable, zProviderSupportedModelsListRequest_unstable, @@ -1257,7 +1415,8 @@ export const zExtResponse = z.union([ zGetToolsResponse_unstable, zGooseToolCallResponse_unstable, zReadResourceResponse_unstable, - zGetExtensionsResponse_unstable, + zGetConfigExtensionsResponse_unstable, + zGetAvailableExtensionsResponse_unstable, zGetSessionExtensionsResponse_unstable, zListProvidersResponse_unstable, zProviderSupportedModelsListResponse_unstable,