mirror of
https://github.com/aaif-goose/goose.git
synced 2026-07-03 14:10:03 +02:00
[goose2] MCP Apps: translate ACP host capabilities into MCP initialization (#8623)
Signed-off-by: Andrew Harvard <aharvard@squareup.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::{generate, Shell as ClapShell};
|
||||
use goose::agents::GoosePlatform;
|
||||
use goose::builtin_extension::register_builtin_extensions;
|
||||
use goose::config::{Config, GooseMode};
|
||||
#[cfg(feature = "telemetry")]
|
||||
@@ -1083,6 +1084,7 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec<String>) ->
|
||||
builtins,
|
||||
data_dir: Paths::data_dir(),
|
||||
config_dir: Paths::config_dir(),
|
||||
goose_platform: GoosePlatform::GooseCli,
|
||||
}));
|
||||
let router = create_router(server);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::acp::fs::AcpTools;
|
||||
use crate::acp::tools::AcpAwareToolMeta;
|
||||
use crate::acp::{PermissionDecision, ACP_CURRENT_MODEL};
|
||||
use crate::agents::extension::{Envs, PLATFORM_EXTENSIONS};
|
||||
use crate::agents::mcp_client::McpClientTrait;
|
||||
use crate::agents::mcp_client::{GooseMcpHostInfo, McpClientTrait};
|
||||
use crate::agents::platform_extensions::developer::DeveloperClient;
|
||||
use crate::agents::{Agent, AgentConfig, ExtensionConfig, GoosePlatform, SessionConfig};
|
||||
use crate::config::base::CONFIG_YAML_NAME;
|
||||
@@ -59,6 +59,7 @@ use sacp::{
|
||||
Agent as SacpAgent, ByteStreams, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled,
|
||||
Responder,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use strum::{EnumMessage, VariantNames};
|
||||
@@ -182,6 +183,7 @@ pub struct GooseAcpAgent {
|
||||
builtins: Vec<String>,
|
||||
client_fs_capabilities: OnceCell<FileSystemCapabilities>,
|
||||
client_terminal: OnceCell<bool>,
|
||||
client_mcp_host_info: OnceCell<GooseMcpHostInfo>,
|
||||
config_dir: std::path::PathBuf,
|
||||
session_manager: Arc<SessionManager>,
|
||||
thread_manager: Arc<crate::session::ThreadManager>,
|
||||
@@ -189,6 +191,7 @@ pub struct GooseAcpAgent {
|
||||
goose_mode: GooseMode,
|
||||
disable_session_naming: bool,
|
||||
provider_inventory: ProviderInventoryService,
|
||||
goose_platform: GoosePlatform,
|
||||
}
|
||||
|
||||
/// Shorten a session/thread id for perf log correlation.
|
||||
@@ -253,6 +256,52 @@ fn extract_timeout_from_meta(meta: &Option<Meta>) -> Option<u64> {
|
||||
.and_then(|v| v.as_u64())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct GooseClientMetaEnvelope {
|
||||
#[serde(default)]
|
||||
goose: Option<GooseClientMeta>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct GooseClientMeta {
|
||||
#[serde(rename = "mcpHostCapabilities", default)]
|
||||
mcp_host_capabilities: Option<GooseMcpHostCapabilities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct GooseMcpHostCapabilities {
|
||||
#[serde(default)]
|
||||
extensions: Option<rmcp::model::ExtensionCapabilities>,
|
||||
}
|
||||
|
||||
fn extract_goose_client_meta(meta: &Meta) -> Option<GooseClientMetaEnvelope> {
|
||||
serde_json::from_value(serde_json::Value::Object(meta.clone())).ok()
|
||||
}
|
||||
|
||||
fn extract_client_mcp_host_info(args: &InitializeRequest) -> GooseMcpHostInfo {
|
||||
let host_capabilities = args
|
||||
.client_capabilities
|
||||
.meta
|
||||
.as_ref()
|
||||
.and_then(extract_goose_client_meta)
|
||||
.and_then(|meta| meta.goose)
|
||||
.and_then(|goose| goose.mcp_host_capabilities);
|
||||
let explicit_extensions = host_capabilities
|
||||
.as_ref()
|
||||
.and_then(|capabilities| capabilities.extensions.as_ref())
|
||||
.is_some();
|
||||
let extensions = host_capabilities
|
||||
.and_then(|capabilities| capabilities.extensions)
|
||||
.unwrap_or_default();
|
||||
|
||||
GooseMcpHostInfo {
|
||||
explicit_extensions,
|
||||
extensions,
|
||||
client_name: args.client_info.as_ref().map(|info| info.name.clone()),
|
||||
client_version: args.client_info.as_ref().map(|info| info.version.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result<ExtensionConfig, String> {
|
||||
match mcp_server {
|
||||
McpServer::Stdio(stdio) => {
|
||||
@@ -795,6 +844,7 @@ impl GooseAcpAgent {
|
||||
config_dir: std::path::PathBuf,
|
||||
goose_mode: GooseMode,
|
||||
disable_session_naming: bool,
|
||||
goose_platform: GoosePlatform,
|
||||
) -> Result<Self> {
|
||||
let session_manager = Arc::new(SessionManager::new(data_dir));
|
||||
let thread_manager = Arc::new(crate::session::ThreadManager::new(
|
||||
@@ -809,6 +859,7 @@ impl GooseAcpAgent {
|
||||
builtins,
|
||||
client_fs_capabilities: OnceCell::new(),
|
||||
client_terminal: OnceCell::new(),
|
||||
client_mcp_host_info: OnceCell::new(),
|
||||
config_dir,
|
||||
session_manager,
|
||||
thread_manager,
|
||||
@@ -816,6 +867,7 @@ impl GooseAcpAgent {
|
||||
goose_mode,
|
||||
disable_session_naming,
|
||||
provider_inventory,
|
||||
goose_platform,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -986,12 +1038,14 @@ impl GooseAcpAgent {
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let client_terminal = self.client_terminal.get().copied().unwrap_or(false);
|
||||
let client_mcp_host_info = self.client_mcp_host_info.get().cloned();
|
||||
let provider_factory = Arc::clone(&self.provider_factory);
|
||||
let disable_session_naming = self.disable_session_naming;
|
||||
let goose_platform = self.goose_platform.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let t_setup = std::time::Instant::now();
|
||||
|
||||
debug!(target: "perf", sid = %sid, "perf: agent_setup start (background)");
|
||||
// Shared config — read once, used by both phases.
|
||||
let config = match Config::new(config_dir.join(CONFIG_YAML_NAME), "goose") {
|
||||
Ok(c) => c,
|
||||
@@ -1005,14 +1059,17 @@ impl GooseAcpAgent {
|
||||
|
||||
// ── Phase 1: create agent + init provider (fast, ~55ms) ──────
|
||||
let phase1: Result<Arc<Agent>, String> = async {
|
||||
let agent = Arc::new(Agent::with_config(AgentConfig::new(
|
||||
session_manager,
|
||||
permission_manager,
|
||||
None,
|
||||
goose_mode,
|
||||
disable_session_naming,
|
||||
GoosePlatform::GooseCli,
|
||||
)));
|
||||
let agent = Arc::new(Agent::with_config(
|
||||
AgentConfig::new(
|
||||
session_manager,
|
||||
permission_manager,
|
||||
None,
|
||||
goose_mode,
|
||||
disable_session_naming,
|
||||
goose_platform,
|
||||
)
|
||||
.with_mcp_host_info(client_mcp_host_info),
|
||||
));
|
||||
|
||||
// Init provider — reuse the pre-resolved name + model when
|
||||
// available (already computed in on_new_session), otherwise
|
||||
@@ -1627,6 +1684,9 @@ impl GooseAcpAgent {
|
||||
.client_fs_capabilities
|
||||
.set(args.client_capabilities.fs.clone());
|
||||
let _ = self.client_terminal.set(args.client_capabilities.terminal);
|
||||
let _ = self
|
||||
.client_mcp_host_info
|
||||
.set(extract_client_mcp_host_info(&args));
|
||||
|
||||
let capabilities = AgentCapabilities::new()
|
||||
.load_session(true)
|
||||
@@ -3962,6 +4022,7 @@ pub async fn run(builtins: Vec<String>) -> Result<()> {
|
||||
builtins,
|
||||
data_dir: Paths::data_dir(),
|
||||
config_dir: Paths::config_dir(),
|
||||
goose_platform: GoosePlatform::GooseCli,
|
||||
},
|
||||
);
|
||||
let agent = server.create_agent().await?;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::acp::server::{AcpProviderFactory, GooseAcpAgent};
|
||||
use crate::agents::GoosePlatform;
|
||||
use anyhow::Result;
|
||||
use std::sync::Arc;
|
||||
use tracing::info;
|
||||
@@ -7,6 +8,7 @@ pub struct AcpServerFactoryConfig {
|
||||
pub builtins: Vec<String>,
|
||||
pub data_dir: std::path::PathBuf,
|
||||
pub config_dir: std::path::PathBuf,
|
||||
pub goose_platform: GoosePlatform,
|
||||
}
|
||||
|
||||
pub struct AcpServer {
|
||||
@@ -44,6 +46,7 @@ impl AcpServer {
|
||||
self.config.config_dir.clone(),
|
||||
goose_mode,
|
||||
disable_session_naming,
|
||||
self.config.goose_platform.clone(),
|
||||
)
|
||||
.await?;
|
||||
info!("Created new ACP agent");
|
||||
|
||||
@@ -12,6 +12,7 @@ use uuid::Uuid;
|
||||
|
||||
use super::container::Container;
|
||||
use super::final_output_tool::FinalOutputTool;
|
||||
use super::mcp_client::GooseMcpHostInfo;
|
||||
use super::platform_tools;
|
||||
use super::tool_confirmation_router::ToolConfirmationRouter;
|
||||
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||
@@ -114,6 +115,7 @@ pub struct AgentConfig {
|
||||
pub goose_mode: GooseMode,
|
||||
pub disable_session_naming: bool,
|
||||
pub goose_platform: GoosePlatform,
|
||||
pub mcp_host_info: Option<GooseMcpHostInfo>,
|
||||
}
|
||||
|
||||
impl AgentConfig {
|
||||
@@ -132,8 +134,14 @@ impl AgentConfig {
|
||||
goose_mode,
|
||||
disable_session_naming,
|
||||
goose_platform,
|
||||
mcp_host_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_mcp_host_info(mut self, mcp_host_info: Option<GooseMcpHostInfo>) -> Self {
|
||||
self.mcp_host_info = mcp_host_info;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The main goose Agent
|
||||
@@ -223,10 +231,23 @@ impl Agent {
|
||||
|
||||
let goose_platform = config.goose_platform.clone();
|
||||
let initial_mode = config.goose_mode;
|
||||
let capabilities = match config.goose_platform {
|
||||
GoosePlatform::GooseDesktop => ExtensionManagerCapabilities { mcpui: true },
|
||||
GoosePlatform::GooseCli => ExtensionManagerCapabilities { mcpui: false },
|
||||
let explicit_mcp_host_info = config.mcp_host_info.clone();
|
||||
let mcpui = explicit_mcp_host_info
|
||||
.as_ref()
|
||||
.filter(|host_info| host_info.explicit_extensions)
|
||||
.map(GooseMcpHostInfo::mcpui_enabled)
|
||||
.unwrap_or_else(|| match config.goose_platform {
|
||||
GoosePlatform::GooseDesktop => true,
|
||||
GoosePlatform::GooseCli => false,
|
||||
});
|
||||
let capabilities = ExtensionManagerCapabilities {
|
||||
mcpui,
|
||||
host_info: explicit_mcp_host_info.clone(),
|
||||
};
|
||||
let client_name = explicit_mcp_host_info
|
||||
.as_ref()
|
||||
.and_then(|host_info| host_info.client_name.clone())
|
||||
.unwrap_or_else(|| goose_platform.to_string());
|
||||
let session_manager = Arc::clone(&config.session_manager);
|
||||
let permission_manager = Arc::clone(&config.permission_manager);
|
||||
Self {
|
||||
@@ -236,7 +257,7 @@ impl Agent {
|
||||
extension_manager: Arc::new(ExtensionManager::new(
|
||||
provider.clone(),
|
||||
session_manager,
|
||||
goose_platform.to_string(),
|
||||
client_name,
|
||||
capabilities,
|
||||
)),
|
||||
final_output_tool: Arc::new(Mutex::new(None)),
|
||||
|
||||
@@ -34,7 +34,9 @@ use super::tool_execution::{ToolCallContext, ToolCallResult};
|
||||
use super::types::SharedProvider;
|
||||
use crate::agents::extension::{Envs, ProcessExit};
|
||||
use crate::agents::extension_malware_check;
|
||||
use crate::agents::mcp_client::{GooseMcpClientCapabilities, McpClient, McpClientTrait};
|
||||
use crate::agents::mcp_client::{
|
||||
GooseMcpClientCapabilities, GooseMcpHostInfo, McpClient, McpClientTrait,
|
||||
};
|
||||
use crate::builtin_extension::get_builtin_extension;
|
||||
use crate::config::extensions::name_to_key;
|
||||
use crate::config::search_path::SearchPaths;
|
||||
@@ -116,6 +118,7 @@ impl Extension {
|
||||
|
||||
pub struct ExtensionManagerCapabilities {
|
||||
pub mcpui: bool,
|
||||
pub host_info: Option<GooseMcpHostInfo>,
|
||||
}
|
||||
|
||||
/// Manages goose extensions / MCP clients and their interactions
|
||||
@@ -591,6 +594,13 @@ async fn create_unix_socket_http_client(
|
||||
}
|
||||
|
||||
impl ExtensionManager {
|
||||
fn mcp_client_capabilities(&self) -> GooseMcpClientCapabilities {
|
||||
GooseMcpClientCapabilities {
|
||||
mcpui: self.capabilities.mcpui,
|
||||
host_info: self.capabilities.host_info.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
provider: SharedProvider,
|
||||
session_manager: Arc<crate::session::SessionManager>,
|
||||
@@ -618,7 +628,10 @@ impl ExtensionManager {
|
||||
Arc::new(Mutex::new(None)),
|
||||
session_manager,
|
||||
"goose-cli".to_string(),
|
||||
ExtensionManagerCapabilities { mcpui: false },
|
||||
ExtensionManagerCapabilities {
|
||||
mcpui: false,
|
||||
host_info: None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -697,10 +710,6 @@ impl ExtensionManager {
|
||||
.map(|(k, v)| (k.clone(), substitute_env_vars(v, &all_envs)))
|
||||
.collect();
|
||||
let resolved_socket = socket.as_ref().map(|s| substitute_env_vars(s, &all_envs));
|
||||
let capability = GooseMcpClientCapabilities {
|
||||
mcpui: self.capabilities.mcpui,
|
||||
};
|
||||
|
||||
create_streamable_http_client(
|
||||
&resolved_uri,
|
||||
*timeout,
|
||||
@@ -709,7 +718,7 @@ impl ExtensionManager {
|
||||
resolved_socket.as_deref(),
|
||||
self.provider.clone(),
|
||||
self.client_name.clone(),
|
||||
capability,
|
||||
self.mcp_client_capabilities(),
|
||||
&effective_working_dir,
|
||||
)
|
||||
.await?
|
||||
@@ -760,10 +769,6 @@ impl ExtensionManager {
|
||||
.arg(&normalized_name);
|
||||
});
|
||||
|
||||
let capabilities = GooseMcpClientCapabilities {
|
||||
mcpui: self.capabilities.mcpui,
|
||||
};
|
||||
|
||||
let client = child_process_client(
|
||||
command,
|
||||
&Some(timeout_secs),
|
||||
@@ -771,7 +776,7 @@ impl ExtensionManager {
|
||||
&effective_working_dir,
|
||||
Some(container_id.to_string()),
|
||||
self.client_name.clone(),
|
||||
capabilities,
|
||||
self.mcp_client_capabilities(),
|
||||
)
|
||||
.await?;
|
||||
Box::new(client)
|
||||
@@ -780,17 +785,13 @@ impl ExtensionManager {
|
||||
let (client_read, server_write) = tokio::io::duplex(65536);
|
||||
extension_fn(server_read, server_write);
|
||||
|
||||
let capabilities = GooseMcpClientCapabilities {
|
||||
mcpui: self.capabilities.mcpui,
|
||||
};
|
||||
|
||||
Box::new(
|
||||
McpClient::connect(
|
||||
(client_read, client_write),
|
||||
Duration::from_secs(timeout_secs),
|
||||
self.provider.clone(),
|
||||
self.client_name.clone(),
|
||||
capabilities,
|
||||
self.mcp_client_capabilities(),
|
||||
effective_working_dir.clone(),
|
||||
)
|
||||
.await?,
|
||||
@@ -840,9 +841,6 @@ impl ExtensionManager {
|
||||
})
|
||||
};
|
||||
|
||||
let capabilities = GooseMcpClientCapabilities {
|
||||
mcpui: self.capabilities.mcpui,
|
||||
};
|
||||
let client = child_process_client(
|
||||
command,
|
||||
timeout,
|
||||
@@ -850,7 +848,7 @@ impl ExtensionManager {
|
||||
&effective_working_dir,
|
||||
container.map(|c| c.id().to_string()),
|
||||
self.client_name.clone(),
|
||||
capabilities,
|
||||
self.mcp_client_capabilities(),
|
||||
)
|
||||
.await?;
|
||||
Box::new(client)
|
||||
@@ -875,10 +873,6 @@ impl ExtensionManager {
|
||||
command.arg("python").arg(file_path.to_str().unwrap());
|
||||
});
|
||||
|
||||
let capabilities = GooseMcpClientCapabilities {
|
||||
mcpui: self.capabilities.mcpui,
|
||||
};
|
||||
|
||||
let client = child_process_client(
|
||||
command,
|
||||
timeout,
|
||||
@@ -886,7 +880,7 @@ impl ExtensionManager {
|
||||
&effective_working_dir,
|
||||
container.map(|c| c.id().to_string()),
|
||||
self.client_name.clone(),
|
||||
capabilities,
|
||||
self.mcp_client_capabilities(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
|
||||
@@ -37,6 +37,34 @@ pub type BoxError = Box<dyn std::error::Error + Sync + Send>;
|
||||
|
||||
pub type Error = rmcp::ServiceError;
|
||||
|
||||
const MCP_APPS_UI_EXTENSION_ID: &str = "io.modelcontextprotocol/ui";
|
||||
const MCP_APPS_UI_MIME_TYPE: &str = "text/html;profile=mcp-app";
|
||||
|
||||
fn default_mcp_apps_ui_extensions() -> ExtensionCapabilities {
|
||||
let mut extensions = ExtensionCapabilities::new();
|
||||
let mut ui_extension_settings = JsonObject::new();
|
||||
ui_extension_settings.insert(
|
||||
"mimeTypes".to_string(),
|
||||
serde_json::json!([MCP_APPS_UI_MIME_TYPE]),
|
||||
);
|
||||
extensions.insert(MCP_APPS_UI_EXTENSION_ID.to_string(), ui_extension_settings);
|
||||
extensions
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct GooseMcpHostInfo {
|
||||
pub explicit_extensions: bool,
|
||||
pub extensions: ExtensionCapabilities,
|
||||
pub client_name: Option<String>,
|
||||
pub client_version: Option<String>,
|
||||
}
|
||||
|
||||
impl GooseMcpHostInfo {
|
||||
pub fn mcpui_enabled(&self) -> bool {
|
||||
self.extensions.contains_key(MCP_APPS_UI_EXTENSION_ID)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait McpClientTrait: Send + Sync {
|
||||
async fn list_tools(
|
||||
@@ -164,6 +192,40 @@ impl GooseClient {
|
||||
.and_then(|(_, value)| value.as_str())
|
||||
.map(|value| value.to_string())
|
||||
}
|
||||
|
||||
fn resolved_extensions(&self) -> ExtensionCapabilities {
|
||||
if let Some(host_info) = &self.capabilities.host_info {
|
||||
if host_info.explicit_extensions {
|
||||
return host_info.extensions.clone();
|
||||
}
|
||||
}
|
||||
|
||||
if self.capabilities.mcpui {
|
||||
return default_mcp_apps_ui_extensions();
|
||||
}
|
||||
|
||||
ExtensionCapabilities::new()
|
||||
}
|
||||
|
||||
fn resolved_client_info(&self) -> Implementation {
|
||||
let name = self
|
||||
.capabilities
|
||||
.host_info
|
||||
.as_ref()
|
||||
.and_then(|host_info| host_info.client_name.clone())
|
||||
.unwrap_or_else(|| self.client_name.clone());
|
||||
let version = self
|
||||
.capabilities
|
||||
.host_info
|
||||
.as_ref()
|
||||
.and_then(|host_info| host_info.client_version.clone())
|
||||
.unwrap_or_else(|| {
|
||||
std::env::var("GOOSE_MCP_CLIENT_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION").to_owned())
|
||||
});
|
||||
|
||||
Implementation::new(name, version)
|
||||
}
|
||||
}
|
||||
|
||||
fn working_dir_roots(dir: &std::path::Path) -> ListRootsResult {
|
||||
@@ -340,21 +402,7 @@ impl ClientHandler for GooseClient {
|
||||
}
|
||||
|
||||
fn get_info(&self) -> ClientInfo {
|
||||
let mut extensions = ExtensionCapabilities::new();
|
||||
|
||||
if self.capabilities.mcpui {
|
||||
// Build MCP Apps UI extension capability
|
||||
// See: https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx
|
||||
let mut ui_extension_settings = JsonObject::new();
|
||||
ui_extension_settings.insert(
|
||||
"mimeTypes".to_string(),
|
||||
serde_json::json!(["text/html;profile=mcp-app"]),
|
||||
);
|
||||
extensions.insert(
|
||||
"io.modelcontextprotocol/ui".to_string(),
|
||||
ui_extension_settings,
|
||||
);
|
||||
}
|
||||
let extensions = self.resolved_extensions();
|
||||
|
||||
InitializeRequestParams::new(
|
||||
ClientCapabilities::builder()
|
||||
@@ -363,11 +411,7 @@ impl ClientHandler for GooseClient {
|
||||
.enable_sampling()
|
||||
.enable_elicitation()
|
||||
.build(),
|
||||
Implementation::new(
|
||||
self.client_name.clone(),
|
||||
std::env::var("GOOSE_MCP_CLIENT_VERSION")
|
||||
.unwrap_or(env!("CARGO_PKG_VERSION").to_owned()),
|
||||
),
|
||||
self.resolved_client_info(),
|
||||
)
|
||||
.with_protocol_version(ProtocolVersion::V_2025_03_26)
|
||||
}
|
||||
@@ -376,6 +420,7 @@ impl ClientHandler for GooseClient {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GooseMcpClientCapabilities {
|
||||
pub mcpui: bool,
|
||||
pub host_info: Option<GooseMcpHostInfo>,
|
||||
}
|
||||
|
||||
/// The MCP client is the interface for MCP operations.
|
||||
@@ -769,8 +814,14 @@ mod tests {
|
||||
|
||||
fn new_client(platform: GoosePlatform) -> GooseClient {
|
||||
let capabilities = match platform {
|
||||
GoosePlatform::GooseDesktop => GooseMcpClientCapabilities { mcpui: true },
|
||||
GoosePlatform::GooseCli => GooseMcpClientCapabilities { mcpui: false },
|
||||
GoosePlatform::GooseDesktop => GooseMcpClientCapabilities {
|
||||
mcpui: true,
|
||||
host_info: None,
|
||||
},
|
||||
GoosePlatform::GooseCli => GooseMcpClientCapabilities {
|
||||
mcpui: false,
|
||||
host_info: None,
|
||||
},
|
||||
};
|
||||
|
||||
GooseClient::new(
|
||||
@@ -1000,6 +1051,93 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_host_info_passes_through_client_identity() {
|
||||
let client = GooseClient::new(
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
Arc::new(Mutex::new(None)),
|
||||
GoosePlatform::GooseDesktop.to_string(),
|
||||
GooseMcpClientCapabilities {
|
||||
mcpui: true,
|
||||
host_info: Some(GooseMcpHostInfo {
|
||||
explicit_extensions: true,
|
||||
extensions: ExtensionCapabilities::new(),
|
||||
client_name: Some("goose2".to_string()),
|
||||
client_version: Some("0.1.0".to_string()),
|
||||
}),
|
||||
},
|
||||
std::env::current_dir().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let info = ClientHandler::get_info(&client);
|
||||
assert_eq!(info.client_info.name, "goose2");
|
||||
assert_eq!(info.client_info.version, "0.1.0");
|
||||
let extensions = info
|
||||
.capabilities
|
||||
.extensions
|
||||
.expect("client should still serialize an extensions object");
|
||||
assert!(
|
||||
!extensions.contains_key(MCP_APPS_UI_EXTENSION_ID),
|
||||
"explicit empty host extensions should disable platform fallback"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_explicit_host_extensions_override_platform_fallback() {
|
||||
let client = GooseClient::new(
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
Arc::new(Mutex::new(None)),
|
||||
GoosePlatform::GooseCli.to_string(),
|
||||
GooseMcpClientCapabilities {
|
||||
mcpui: false,
|
||||
host_info: Some(GooseMcpHostInfo {
|
||||
explicit_extensions: true,
|
||||
extensions: default_mcp_apps_ui_extensions(),
|
||||
client_name: Some("goose2".to_string()),
|
||||
client_version: Some("0.1.0".to_string()),
|
||||
}),
|
||||
},
|
||||
std::env::current_dir().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let info = ClientHandler::get_info(&client);
|
||||
let extensions = info
|
||||
.capabilities
|
||||
.extensions
|
||||
.expect("capabilities should have explicit host extensions");
|
||||
|
||||
assert!(extensions.contains_key(MCP_APPS_UI_EXTENSION_ID));
|
||||
assert_eq!(info.client_info.name, "goose2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_identity_does_not_disable_platform_fallback_without_explicit_extensions() {
|
||||
let client = GooseClient::new(
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
Arc::new(Mutex::new(None)),
|
||||
GoosePlatform::GooseDesktop.to_string(),
|
||||
GooseMcpClientCapabilities {
|
||||
mcpui: true,
|
||||
host_info: Some(GooseMcpHostInfo {
|
||||
explicit_extensions: false,
|
||||
extensions: ExtensionCapabilities::new(),
|
||||
client_name: Some("goose2".to_string()),
|
||||
client_version: Some("0.1.0".to_string()),
|
||||
}),
|
||||
},
|
||||
std::env::current_dir().unwrap_or_default(),
|
||||
);
|
||||
|
||||
let info = ClientHandler::get_info(&client);
|
||||
let extensions = info
|
||||
.capabilities
|
||||
.extensions
|
||||
.expect("platform fallback should still advertise MCP Apps UI");
|
||||
|
||||
assert!(extensions.contains_key(MCP_APPS_UI_EXTENSION_ID));
|
||||
assert_eq!(info.client_info.name, "goose2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_working_dir_roots_returns_current_dir_as_root() {
|
||||
let dir = PathBuf::from("/tmp/test-project");
|
||||
|
||||
@@ -5,6 +5,7 @@ use async_trait::async_trait;
|
||||
use fs_err as fs;
|
||||
use goose::acp::server::{serve, AcpProviderFactory, GooseAcpAgent};
|
||||
pub use goose::acp::{map_permission_response, PermissionDecision};
|
||||
use goose::agents::GoosePlatform;
|
||||
use goose::builtin_extension::register_builtin_extensions;
|
||||
use goose::config::paths::Paths;
|
||||
use goose::config::{GooseMode, PermissionManager};
|
||||
@@ -190,6 +191,7 @@ pub async fn spawn_acp_server_in_process(
|
||||
data_root.to_path_buf(),
|
||||
goose_mode,
|
||||
true,
|
||||
GoosePlatform::GooseCli,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -261,7 +261,10 @@ async fn test_replayed_session(
|
||||
provider,
|
||||
session_manager,
|
||||
GoosePlatform::GooseDesktop.to_string(),
|
||||
ExtensionManagerCapabilities { mcpui: true },
|
||||
ExtensionManagerCapabilities {
|
||||
mcpui: true,
|
||||
host_info: None,
|
||||
},
|
||||
));
|
||||
|
||||
#[allow(clippy::redundant_closure_call)]
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { GooseClient } from "@aaif/goose-sdk";
|
||||
import {
|
||||
DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,
|
||||
GooseClient,
|
||||
type GooseInitializeRequest,
|
||||
} from "@aaif/goose-sdk";
|
||||
import {
|
||||
PROTOCOL_VERSION,
|
||||
type Client,
|
||||
@@ -7,6 +11,7 @@ import {
|
||||
type RequestPermissionRequest,
|
||||
type RequestPermissionResponse,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import packageJson from "../../../package.json";
|
||||
import { createWebSocketStream } from "./createWebSocketStream";
|
||||
import { perfLog } from "@/shared/lib/perfLog";
|
||||
|
||||
@@ -81,12 +86,18 @@ async function initializeConnection(): Promise<GooseClient> {
|
||||
const tInit = performance.now();
|
||||
await client.initialize({
|
||||
protocolVersion: PROTOCOL_VERSION,
|
||||
clientCapabilities: {},
|
||||
clientInfo: {
|
||||
name: "goose2",
|
||||
version: "0.1.0",
|
||||
clientCapabilities: {
|
||||
_meta: {
|
||||
goose: {
|
||||
mcpHostCapabilities: DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
clientInfo: {
|
||||
name: packageJson.name,
|
||||
version: packageJson.version,
|
||||
},
|
||||
} satisfies GooseInitializeRequest);
|
||||
perfLog(
|
||||
`[perf:conn] client.initialize in ${(performance.now() - tInit).toFixed(1)}ms (total ${(performance.now() - tStart).toFixed(1)}ms)`,
|
||||
);
|
||||
|
||||
Generated
+3
@@ -648,6 +648,9 @@ importers:
|
||||
|
||||
sdk:
|
||||
dependencies:
|
||||
'@modelcontextprotocol/ext-apps':
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1(@modelcontextprotocol/sdk@1.27.1(zod@3.25.76))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@3.25.76)
|
||||
zod:
|
||||
specifier: ^3.25.76
|
||||
version: 3.25.76
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/ext-apps": "^0.3.1",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -2,6 +2,7 @@ export * from "./generated/types.gen.js";
|
||||
export * from "./generated/zod.gen.js";
|
||||
export { GooseClient } from "./goose-client.js";
|
||||
export { createHttpStream } from "./http-stream.js";
|
||||
export * from "./mcp-apps.js";
|
||||
|
||||
export {
|
||||
ClientSideConnection,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
Implementation,
|
||||
InitializeRequest,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps";
|
||||
|
||||
export const GOOSE_MCP_UI_EXTENSION_ID = "io.modelcontextprotocol/ui" as const;
|
||||
|
||||
export interface GooseMcpUiExtensionSettings {
|
||||
mimeTypes: string[];
|
||||
}
|
||||
|
||||
export interface GooseMcpHostCapabilities {
|
||||
extensions: Record<string, GooseMcpUiExtensionSettings>;
|
||||
}
|
||||
|
||||
export interface GooseClientMeta {
|
||||
goose: {
|
||||
mcpHostCapabilities: GooseMcpHostCapabilities;
|
||||
};
|
||||
}
|
||||
|
||||
export type GooseInitializeRequest = InitializeRequest & {
|
||||
clientCapabilities: NonNullable<InitializeRequest["clientCapabilities"]> & {
|
||||
_meta: GooseClientMeta;
|
||||
};
|
||||
clientInfo: Implementation;
|
||||
};
|
||||
|
||||
export const DEFAULT_GOOSE_MCP_HOST_CAPABILITIES: GooseMcpHostCapabilities = {
|
||||
extensions: {
|
||||
[GOOSE_MCP_UI_EXTENSION_ID]: {
|
||||
mimeTypes: [RESOURCE_MIME_TYPE],
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user