From f9088374079a656b0ac9aba7a33efa39245ef83a Mon Sep 17 00:00:00 2001 From: Andrew Harvard Date: Tue, 28 Apr 2026 08:03:44 -0400 Subject: [PATCH] [goose2] MCP Apps: translate ACP host capabilities into MCP initialization (#8623) Signed-off-by: Andrew Harvard --- crates/goose-cli/src/cli.rs | 2 + crates/goose/src/acp/server.rs | 81 ++++++++- crates/goose/src/acp/server_factory.rs | 3 + crates/goose/src/agents/agent.rs | 29 ++- crates/goose/src/agents/extension_manager.rs | 46 ++--- crates/goose/src/agents/mcp_client.rs | 182 ++++++++++++++++--- crates/goose/tests/acp_fixtures/mod.rs | 2 + crates/goose/tests/mcp_integration_test.rs | 5 +- ui/goose2/src/shared/api/acpConnection.ts | 23 ++- ui/pnpm-lock.yaml | 3 + ui/sdk/package.json | 1 + ui/sdk/src/index.ts | 1 + ui/sdk/src/mcp-apps.ts | 36 ++++ 13 files changed, 345 insertions(+), 69 deletions(-) create mode 100644 ui/sdk/src/mcp-apps.ts diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index ecf7f81d76..88ff8685b8 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -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) -> builtins, data_dir: Paths::data_dir(), config_dir: Paths::config_dir(), + goose_platform: GoosePlatform::GooseCli, })); let router = create_router(server); diff --git a/crates/goose/src/acp/server.rs b/crates/goose/src/acp/server.rs index a7c873a1fe..a90c8fbf3c 100644 --- a/crates/goose/src/acp/server.rs +++ b/crates/goose/src/acp/server.rs @@ -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, client_fs_capabilities: OnceCell, client_terminal: OnceCell, + client_mcp_host_info: OnceCell, config_dir: std::path::PathBuf, session_manager: Arc, thread_manager: Arc, @@ -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) -> Option { .and_then(|v| v.as_u64()) } +#[derive(Debug, Default, Deserialize)] +struct GooseClientMetaEnvelope { + #[serde(default)] + goose: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct GooseClientMeta { + #[serde(rename = "mcpHostCapabilities", default)] + mcp_host_capabilities: Option, +} + +#[derive(Debug, Default, Deserialize)] +struct GooseMcpHostCapabilities { + #[serde(default)] + extensions: Option, +} + +fn extract_goose_client_meta(meta: &Meta) -> Option { + 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 { 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 { 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, 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) -> Result<()> { builtins, data_dir: Paths::data_dir(), config_dir: Paths::config_dir(), + goose_platform: GoosePlatform::GooseCli, }, ); let agent = server.create_agent().await?; diff --git a/crates/goose/src/acp/server_factory.rs b/crates/goose/src/acp/server_factory.rs index dbbc9d1a07..2e9686a708 100644 --- a/crates/goose/src/acp/server_factory.rs +++ b/crates/goose/src/acp/server_factory.rs @@ -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, 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"); diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index 990cfa20d4..57d8da542c 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -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, } 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) -> 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)), diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index e534c0a3b8..c9f25963a7 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -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, } /// 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, @@ -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?; diff --git a/crates/goose/src/agents/mcp_client.rs b/crates/goose/src/agents/mcp_client.rs index aa604d2dd3..a0a7ac8fc1 100644 --- a/crates/goose/src/agents/mcp_client.rs +++ b/crates/goose/src/agents/mcp_client.rs @@ -37,6 +37,34 @@ pub type BoxError = Box; 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, + pub client_version: Option, +} + +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, } /// 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"); diff --git a/crates/goose/tests/acp_fixtures/mod.rs b/crates/goose/tests/acp_fixtures/mod.rs index ab763edce9..082ae81664 100644 --- a/crates/goose/tests/acp_fixtures/mod.rs +++ b/crates/goose/tests/acp_fixtures/mod.rs @@ -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(); diff --git a/crates/goose/tests/mcp_integration_test.rs b/crates/goose/tests/mcp_integration_test.rs index 693f31f941..b85931c44a 100644 --- a/crates/goose/tests/mcp_integration_test.rs +++ b/crates/goose/tests/mcp_integration_test.rs @@ -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)] diff --git a/ui/goose2/src/shared/api/acpConnection.ts b/ui/goose2/src/shared/api/acpConnection.ts index 5a369d3d8c..e9cfa90923 100644 --- a/ui/goose2/src/shared/api/acpConnection.ts +++ b/ui/goose2/src/shared/api/acpConnection.ts @@ -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 { 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)`, ); diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 231b3dd8f9..2584e3f121 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -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 diff --git a/ui/sdk/package.json b/ui/sdk/package.json index 91ae373f39..19a2380eb7 100644 --- a/ui/sdk/package.json +++ b/ui/sdk/package.json @@ -40,6 +40,7 @@ "format": "prettier --write src/" }, "dependencies": { + "@modelcontextprotocol/ext-apps": "^0.3.1", "zod": "^3.25.76" }, "peerDependencies": { diff --git a/ui/sdk/src/index.ts b/ui/sdk/src/index.ts index 9e4f469d24..5e587ed92e 100644 --- a/ui/sdk/src/index.ts +++ b/ui/sdk/src/index.ts @@ -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, diff --git a/ui/sdk/src/mcp-apps.ts b/ui/sdk/src/mcp-apps.ts new file mode 100644 index 0000000000..6d3b887ff2 --- /dev/null +++ b/ui/sdk/src/mcp-apps.ts @@ -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; +} + +export interface GooseClientMeta { + goose: { + mcpHostCapabilities: GooseMcpHostCapabilities; + }; +} + +export type GooseInitializeRequest = InitializeRequest & { + clientCapabilities: NonNullable & { + _meta: GooseClientMeta; + }; + clientInfo: Implementation; +}; + +export const DEFAULT_GOOSE_MCP_HOST_CAPABILITIES: GooseMcpHostCapabilities = { + extensions: { + [GOOSE_MCP_UI_EXTENSION_ID]: { + mimeTypes: [RESOURCE_MIME_TYPE], + }, + }, +};