feat: acp methods for config extensions (#9581)

This commit is contained in:
Lifei Zhou
2026-06-05 18:08:19 +10:00
committed by GitHub
parent 65b72b2988
commit 2f99664222
15 changed files with 2046 additions and 157 deletions
+92 -14
View File
@@ -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<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bundled: Option<bool>,
},
Platform {
name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bundled: Option<bool>,
},
Mcp {
server: McpServer,
#[serde(default, rename = "envKeys", skip_serializing_if = "Vec::is_empty")]
env_keys: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
timeout: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
socket: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
bundled: Option<bool>,
},
}
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<String>,
}
/// 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<GooseExtension>,
}
/// 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<serde_json::Value>,
pub struct GetConfigExtensionsResponse {
pub extensions: Vec<GooseExtensionEntry>,
#[serde(default)]
pub warnings: Vec<String>,
}
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,
}
+9 -4
View File
@@ -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"
},
{
+417 -26
View File
@@ -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": [
+15 -8
View File
@@ -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<GetExtensionsResponse, agent_client_protocol::Error> {
self.on_get_extensions().await
) -> Result<GetConfigExtensionsResponse, agent_client_protocol::Error> {
self.on_get_config_extensions().await
}
#[custom_method(GetAvailableExtensionsRequest)]
async fn dispatch_get_available_extensions(
&self,
) -> Result<GetAvailableExtensionsResponse, agent_client_protocol::Error> {
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<EmptyResponse, agent_client_protocol::Error> {
self.on_toggle_config_extension(req).await
self.on_set_config_extension_enabled(req).await
}
#[custom_method(GetSessionExtensionsRequest)]
+691 -45
View File
@@ -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<GetExtensionsResponse, agent_client_protocol::Error> {
) -> Result<GetConfigExtensionsResponse, agent_client_protocol::Error> {
let extensions = crate::config::extensions::get_all_extensions()
.into_iter()
.filter(|ext| {
@@ -40,51 +43,46 @@ impl GooseAcpAgent {
})
.collect::<Vec<_>>();
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::<Result<Vec<_>, _>>()
.internal_err()?;
Ok(GetExtensionsResponse {
extensions: extensions_json,
.map(config_entry_to_goose_entry)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
Ok(GetConfigExtensionsResponse {
extensions,
warnings,
})
}
pub(super) async fn on_get_available_extensions(
&self,
) -> Result<GetAvailableExtensionsResponse, agent_client_protocol::Error> {
let extensions = crate::config::get_available_extensions()
.into_iter()
.map(|config| config_to_goose_extension(&config))
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.flatten()
.collect::<Vec<_>>();
Ok(GetAvailableExtensionsResponse { extensions })
}
pub(super) async fn on_add_config_extension(
&self,
req: AddConfigExtensionRequest,
) -> Result<EmptyResponse, agent_client_protocol::Error> {
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<EmptyResponse, agent_client_protocol::Error> {
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<EmptyResponse, agent_client_protocol::Error> {
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<Option<GooseExtension>, 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<ConfigExtensionConversion, agent_client_protocol::Error> {
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<Option<GooseExtensionEntry>, 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<String> {
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());
}
}
+149 -1
View File
@@ -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<String, Value>) {
add_object_discriminant(defs, "McpServerHttp", "http");
add_object_discriminant(defs, "McpServerSse", "sse");
}
fn add_object_discriminant(defs: &mut Map<String, Value>, 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"
);
}
}
+4
View File
@@ -22,3 +22,7 @@ pub fn register_builtin_extensions(extensions: HashMap<&'static str, SpawnServer
pub fn get_builtin_extension(name: &str) -> Option<SpawnServerFn> {
BUILTIN_REGISTRY.read().unwrap().get(name).cloned()
}
pub fn get_builtin_extension_names() -> Vec<&'static str> {
BUILTIN_REGISTRY.read().unwrap().keys().copied().collect()
}
+43 -5
View File
@@ -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<ExtensionEntry> {
@@ -145,6 +149,40 @@ pub fn get_enabled_extensions_with_config(config: &Config) -> Vec<ExtensionConfi
.collect()
}
pub fn get_available_extensions() -> Vec<ExtensionConfig> {
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::<Vec<_>>();
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<String> {
let raw: Mapping = Config::global()
.get_param(EXTENSIONS_CONFIG_KEY)
+3 -3
View File
@@ -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;
+154 -6
View File
@@ -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<tempfile::TempDir> =
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::<String>("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"
);
});
}
+68 -2
View File
@@ -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<ExtensionResponse> {
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 ?? [],
};
}
+26 -11
View File
@@ -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<GetExtensionsResponse_unstable> {
params: GetConfigExtensionsRequest_unstable,
): Promise<GetConfigExtensionsResponse_unstable> {
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<GetAvailableExtensionsResponse_unstable> {
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<void> {
await this.conn.extMethod(
"_goose/unstable/config/extensions/toggle",
"_goose/unstable/config/extensions/set-enabled",
params,
);
}
+10 -5
View File
@@ -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",
},
{
+195 -16
View File
@@ -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<GooseExtensionEntry>;
warnings?: Array<string>;
};
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<string>;
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<unknown>;
warnings: Array<string>;
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<HttpHeader>;
/**
* 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<HttpHeader>;
/**
* 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<string>;
/**
* Environment variables to set when launching the MCP server.
*/
env: Array<EnvVariable>;
/**
* 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<GooseExtension>;
};
/**
* 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;
+170 -11
View File
@@ -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,