[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 anyhow::Result;
|
||||||
use clap::{Args, CommandFactory, Parser, Subcommand};
|
use clap::{Args, CommandFactory, Parser, Subcommand};
|
||||||
use clap_complete::{generate, Shell as ClapShell};
|
use clap_complete::{generate, Shell as ClapShell};
|
||||||
|
use goose::agents::GoosePlatform;
|
||||||
use goose::builtin_extension::register_builtin_extensions;
|
use goose::builtin_extension::register_builtin_extensions;
|
||||||
use goose::config::{Config, GooseMode};
|
use goose::config::{Config, GooseMode};
|
||||||
#[cfg(feature = "telemetry")]
|
#[cfg(feature = "telemetry")]
|
||||||
@@ -1083,6 +1084,7 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec<String>) ->
|
|||||||
builtins,
|
builtins,
|
||||||
data_dir: Paths::data_dir(),
|
data_dir: Paths::data_dir(),
|
||||||
config_dir: Paths::config_dir(),
|
config_dir: Paths::config_dir(),
|
||||||
|
goose_platform: GoosePlatform::GooseCli,
|
||||||
}));
|
}));
|
||||||
let router = create_router(server);
|
let router = create_router(server);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::acp::fs::AcpTools;
|
|||||||
use crate::acp::tools::AcpAwareToolMeta;
|
use crate::acp::tools::AcpAwareToolMeta;
|
||||||
use crate::acp::{PermissionDecision, ACP_CURRENT_MODEL};
|
use crate::acp::{PermissionDecision, ACP_CURRENT_MODEL};
|
||||||
use crate::agents::extension::{Envs, PLATFORM_EXTENSIONS};
|
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::platform_extensions::developer::DeveloperClient;
|
||||||
use crate::agents::{Agent, AgentConfig, ExtensionConfig, GoosePlatform, SessionConfig};
|
use crate::agents::{Agent, AgentConfig, ExtensionConfig, GoosePlatform, SessionConfig};
|
||||||
use crate::config::base::CONFIG_YAML_NAME;
|
use crate::config::base::CONFIG_YAML_NAME;
|
||||||
@@ -59,6 +59,7 @@ use sacp::{
|
|||||||
Agent as SacpAgent, ByteStreams, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled,
|
Agent as SacpAgent, ByteStreams, Client, ConnectionTo, Dispatch, HandleDispatchFrom, Handled,
|
||||||
Responder,
|
Responder,
|
||||||
};
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use strum::{EnumMessage, VariantNames};
|
use strum::{EnumMessage, VariantNames};
|
||||||
@@ -182,6 +183,7 @@ pub struct GooseAcpAgent {
|
|||||||
builtins: Vec<String>,
|
builtins: Vec<String>,
|
||||||
client_fs_capabilities: OnceCell<FileSystemCapabilities>,
|
client_fs_capabilities: OnceCell<FileSystemCapabilities>,
|
||||||
client_terminal: OnceCell<bool>,
|
client_terminal: OnceCell<bool>,
|
||||||
|
client_mcp_host_info: OnceCell<GooseMcpHostInfo>,
|
||||||
config_dir: std::path::PathBuf,
|
config_dir: std::path::PathBuf,
|
||||||
session_manager: Arc<SessionManager>,
|
session_manager: Arc<SessionManager>,
|
||||||
thread_manager: Arc<crate::session::ThreadManager>,
|
thread_manager: Arc<crate::session::ThreadManager>,
|
||||||
@@ -189,6 +191,7 @@ pub struct GooseAcpAgent {
|
|||||||
goose_mode: GooseMode,
|
goose_mode: GooseMode,
|
||||||
disable_session_naming: bool,
|
disable_session_naming: bool,
|
||||||
provider_inventory: ProviderInventoryService,
|
provider_inventory: ProviderInventoryService,
|
||||||
|
goose_platform: GoosePlatform,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shorten a session/thread id for perf log correlation.
|
/// 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())
|
.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> {
|
fn mcp_server_to_extension_config(mcp_server: McpServer) -> Result<ExtensionConfig, String> {
|
||||||
match mcp_server {
|
match mcp_server {
|
||||||
McpServer::Stdio(stdio) => {
|
McpServer::Stdio(stdio) => {
|
||||||
@@ -795,6 +844,7 @@ impl GooseAcpAgent {
|
|||||||
config_dir: std::path::PathBuf,
|
config_dir: std::path::PathBuf,
|
||||||
goose_mode: GooseMode,
|
goose_mode: GooseMode,
|
||||||
disable_session_naming: bool,
|
disable_session_naming: bool,
|
||||||
|
goose_platform: GoosePlatform,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let session_manager = Arc::new(SessionManager::new(data_dir));
|
let session_manager = Arc::new(SessionManager::new(data_dir));
|
||||||
let thread_manager = Arc::new(crate::session::ThreadManager::new(
|
let thread_manager = Arc::new(crate::session::ThreadManager::new(
|
||||||
@@ -809,6 +859,7 @@ impl GooseAcpAgent {
|
|||||||
builtins,
|
builtins,
|
||||||
client_fs_capabilities: OnceCell::new(),
|
client_fs_capabilities: OnceCell::new(),
|
||||||
client_terminal: OnceCell::new(),
|
client_terminal: OnceCell::new(),
|
||||||
|
client_mcp_host_info: OnceCell::new(),
|
||||||
config_dir,
|
config_dir,
|
||||||
session_manager,
|
session_manager,
|
||||||
thread_manager,
|
thread_manager,
|
||||||
@@ -816,6 +867,7 @@ impl GooseAcpAgent {
|
|||||||
goose_mode,
|
goose_mode,
|
||||||
disable_session_naming,
|
disable_session_naming,
|
||||||
provider_inventory,
|
provider_inventory,
|
||||||
|
goose_platform,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -986,12 +1038,14 @@ impl GooseAcpAgent {
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let client_terminal = self.client_terminal.get().copied().unwrap_or(false);
|
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 provider_factory = Arc::clone(&self.provider_factory);
|
||||||
let disable_session_naming = self.disable_session_naming;
|
let disable_session_naming = self.disable_session_naming;
|
||||||
|
let goose_platform = self.goose_platform.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let t_setup = std::time::Instant::now();
|
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.
|
// Shared config — read once, used by both phases.
|
||||||
let config = match Config::new(config_dir.join(CONFIG_YAML_NAME), "goose") {
|
let config = match Config::new(config_dir.join(CONFIG_YAML_NAME), "goose") {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -1005,14 +1059,17 @@ impl GooseAcpAgent {
|
|||||||
|
|
||||||
// ── Phase 1: create agent + init provider (fast, ~55ms) ──────
|
// ── Phase 1: create agent + init provider (fast, ~55ms) ──────
|
||||||
let phase1: Result<Arc<Agent>, String> = async {
|
let phase1: Result<Arc<Agent>, String> = async {
|
||||||
let agent = Arc::new(Agent::with_config(AgentConfig::new(
|
let agent = Arc::new(Agent::with_config(
|
||||||
session_manager,
|
AgentConfig::new(
|
||||||
permission_manager,
|
session_manager,
|
||||||
None,
|
permission_manager,
|
||||||
goose_mode,
|
None,
|
||||||
disable_session_naming,
|
goose_mode,
|
||||||
GoosePlatform::GooseCli,
|
disable_session_naming,
|
||||||
)));
|
goose_platform,
|
||||||
|
)
|
||||||
|
.with_mcp_host_info(client_mcp_host_info),
|
||||||
|
));
|
||||||
|
|
||||||
// Init provider — reuse the pre-resolved name + model when
|
// Init provider — reuse the pre-resolved name + model when
|
||||||
// available (already computed in on_new_session), otherwise
|
// available (already computed in on_new_session), otherwise
|
||||||
@@ -1627,6 +1684,9 @@ impl GooseAcpAgent {
|
|||||||
.client_fs_capabilities
|
.client_fs_capabilities
|
||||||
.set(args.client_capabilities.fs.clone());
|
.set(args.client_capabilities.fs.clone());
|
||||||
let _ = self.client_terminal.set(args.client_capabilities.terminal);
|
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()
|
let capabilities = AgentCapabilities::new()
|
||||||
.load_session(true)
|
.load_session(true)
|
||||||
@@ -3962,6 +4022,7 @@ pub async fn run(builtins: Vec<String>) -> Result<()> {
|
|||||||
builtins,
|
builtins,
|
||||||
data_dir: Paths::data_dir(),
|
data_dir: Paths::data_dir(),
|
||||||
config_dir: Paths::config_dir(),
|
config_dir: Paths::config_dir(),
|
||||||
|
goose_platform: GoosePlatform::GooseCli,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let agent = server.create_agent().await?;
|
let agent = server.create_agent().await?;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::acp::server::{AcpProviderFactory, GooseAcpAgent};
|
use crate::acp::server::{AcpProviderFactory, GooseAcpAgent};
|
||||||
|
use crate::agents::GoosePlatform;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
@@ -7,6 +8,7 @@ pub struct AcpServerFactoryConfig {
|
|||||||
pub builtins: Vec<String>,
|
pub builtins: Vec<String>,
|
||||||
pub data_dir: std::path::PathBuf,
|
pub data_dir: std::path::PathBuf,
|
||||||
pub config_dir: std::path::PathBuf,
|
pub config_dir: std::path::PathBuf,
|
||||||
|
pub goose_platform: GoosePlatform,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AcpServer {
|
pub struct AcpServer {
|
||||||
@@ -44,6 +46,7 @@ impl AcpServer {
|
|||||||
self.config.config_dir.clone(),
|
self.config.config_dir.clone(),
|
||||||
goose_mode,
|
goose_mode,
|
||||||
disable_session_naming,
|
disable_session_naming,
|
||||||
|
self.config.goose_platform.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
info!("Created new ACP agent");
|
info!("Created new ACP agent");
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use super::container::Container;
|
use super::container::Container;
|
||||||
use super::final_output_tool::FinalOutputTool;
|
use super::final_output_tool::FinalOutputTool;
|
||||||
|
use super::mcp_client::GooseMcpHostInfo;
|
||||||
use super::platform_tools;
|
use super::platform_tools;
|
||||||
use super::tool_confirmation_router::ToolConfirmationRouter;
|
use super::tool_confirmation_router::ToolConfirmationRouter;
|
||||||
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
|
||||||
@@ -114,6 +115,7 @@ pub struct AgentConfig {
|
|||||||
pub goose_mode: GooseMode,
|
pub goose_mode: GooseMode,
|
||||||
pub disable_session_naming: bool,
|
pub disable_session_naming: bool,
|
||||||
pub goose_platform: GoosePlatform,
|
pub goose_platform: GoosePlatform,
|
||||||
|
pub mcp_host_info: Option<GooseMcpHostInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AgentConfig {
|
impl AgentConfig {
|
||||||
@@ -132,8 +134,14 @@ impl AgentConfig {
|
|||||||
goose_mode,
|
goose_mode,
|
||||||
disable_session_naming,
|
disable_session_naming,
|
||||||
goose_platform,
|
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
|
/// The main goose Agent
|
||||||
@@ -223,10 +231,23 @@ impl Agent {
|
|||||||
|
|
||||||
let goose_platform = config.goose_platform.clone();
|
let goose_platform = config.goose_platform.clone();
|
||||||
let initial_mode = config.goose_mode;
|
let initial_mode = config.goose_mode;
|
||||||
let capabilities = match config.goose_platform {
|
let explicit_mcp_host_info = config.mcp_host_info.clone();
|
||||||
GoosePlatform::GooseDesktop => ExtensionManagerCapabilities { mcpui: true },
|
let mcpui = explicit_mcp_host_info
|
||||||
GoosePlatform::GooseCli => ExtensionManagerCapabilities { mcpui: false },
|
.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 session_manager = Arc::clone(&config.session_manager);
|
||||||
let permission_manager = Arc::clone(&config.permission_manager);
|
let permission_manager = Arc::clone(&config.permission_manager);
|
||||||
Self {
|
Self {
|
||||||
@@ -236,7 +257,7 @@ impl Agent {
|
|||||||
extension_manager: Arc::new(ExtensionManager::new(
|
extension_manager: Arc::new(ExtensionManager::new(
|
||||||
provider.clone(),
|
provider.clone(),
|
||||||
session_manager,
|
session_manager,
|
||||||
goose_platform.to_string(),
|
client_name,
|
||||||
capabilities,
|
capabilities,
|
||||||
)),
|
)),
|
||||||
final_output_tool: Arc::new(Mutex::new(None)),
|
final_output_tool: Arc::new(Mutex::new(None)),
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ use super::tool_execution::{ToolCallContext, ToolCallResult};
|
|||||||
use super::types::SharedProvider;
|
use super::types::SharedProvider;
|
||||||
use crate::agents::extension::{Envs, ProcessExit};
|
use crate::agents::extension::{Envs, ProcessExit};
|
||||||
use crate::agents::extension_malware_check;
|
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::builtin_extension::get_builtin_extension;
|
||||||
use crate::config::extensions::name_to_key;
|
use crate::config::extensions::name_to_key;
|
||||||
use crate::config::search_path::SearchPaths;
|
use crate::config::search_path::SearchPaths;
|
||||||
@@ -116,6 +118,7 @@ impl Extension {
|
|||||||
|
|
||||||
pub struct ExtensionManagerCapabilities {
|
pub struct ExtensionManagerCapabilities {
|
||||||
pub mcpui: bool,
|
pub mcpui: bool,
|
||||||
|
pub host_info: Option<GooseMcpHostInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manages goose extensions / MCP clients and their interactions
|
/// Manages goose extensions / MCP clients and their interactions
|
||||||
@@ -591,6 +594,13 @@ async fn create_unix_socket_http_client(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionManager {
|
impl ExtensionManager {
|
||||||
|
fn mcp_client_capabilities(&self) -> GooseMcpClientCapabilities {
|
||||||
|
GooseMcpClientCapabilities {
|
||||||
|
mcpui: self.capabilities.mcpui,
|
||||||
|
host_info: self.capabilities.host_info.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
provider: SharedProvider,
|
provider: SharedProvider,
|
||||||
session_manager: Arc<crate::session::SessionManager>,
|
session_manager: Arc<crate::session::SessionManager>,
|
||||||
@@ -618,7 +628,10 @@ impl ExtensionManager {
|
|||||||
Arc::new(Mutex::new(None)),
|
Arc::new(Mutex::new(None)),
|
||||||
session_manager,
|
session_manager,
|
||||||
"goose-cli".to_string(),
|
"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)))
|
.map(|(k, v)| (k.clone(), substitute_env_vars(v, &all_envs)))
|
||||||
.collect();
|
.collect();
|
||||||
let resolved_socket = socket.as_ref().map(|s| substitute_env_vars(s, &all_envs));
|
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(
|
create_streamable_http_client(
|
||||||
&resolved_uri,
|
&resolved_uri,
|
||||||
*timeout,
|
*timeout,
|
||||||
@@ -709,7 +718,7 @@ impl ExtensionManager {
|
|||||||
resolved_socket.as_deref(),
|
resolved_socket.as_deref(),
|
||||||
self.provider.clone(),
|
self.provider.clone(),
|
||||||
self.client_name.clone(),
|
self.client_name.clone(),
|
||||||
capability,
|
self.mcp_client_capabilities(),
|
||||||
&effective_working_dir,
|
&effective_working_dir,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
@@ -760,10 +769,6 @@ impl ExtensionManager {
|
|||||||
.arg(&normalized_name);
|
.arg(&normalized_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
let capabilities = GooseMcpClientCapabilities {
|
|
||||||
mcpui: self.capabilities.mcpui,
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = child_process_client(
|
let client = child_process_client(
|
||||||
command,
|
command,
|
||||||
&Some(timeout_secs),
|
&Some(timeout_secs),
|
||||||
@@ -771,7 +776,7 @@ impl ExtensionManager {
|
|||||||
&effective_working_dir,
|
&effective_working_dir,
|
||||||
Some(container_id.to_string()),
|
Some(container_id.to_string()),
|
||||||
self.client_name.clone(),
|
self.client_name.clone(),
|
||||||
capabilities,
|
self.mcp_client_capabilities(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Box::new(client)
|
Box::new(client)
|
||||||
@@ -780,17 +785,13 @@ impl ExtensionManager {
|
|||||||
let (client_read, server_write) = tokio::io::duplex(65536);
|
let (client_read, server_write) = tokio::io::duplex(65536);
|
||||||
extension_fn(server_read, server_write);
|
extension_fn(server_read, server_write);
|
||||||
|
|
||||||
let capabilities = GooseMcpClientCapabilities {
|
|
||||||
mcpui: self.capabilities.mcpui,
|
|
||||||
};
|
|
||||||
|
|
||||||
Box::new(
|
Box::new(
|
||||||
McpClient::connect(
|
McpClient::connect(
|
||||||
(client_read, client_write),
|
(client_read, client_write),
|
||||||
Duration::from_secs(timeout_secs),
|
Duration::from_secs(timeout_secs),
|
||||||
self.provider.clone(),
|
self.provider.clone(),
|
||||||
self.client_name.clone(),
|
self.client_name.clone(),
|
||||||
capabilities,
|
self.mcp_client_capabilities(),
|
||||||
effective_working_dir.clone(),
|
effective_working_dir.clone(),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
@@ -840,9 +841,6 @@ impl ExtensionManager {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let capabilities = GooseMcpClientCapabilities {
|
|
||||||
mcpui: self.capabilities.mcpui,
|
|
||||||
};
|
|
||||||
let client = child_process_client(
|
let client = child_process_client(
|
||||||
command,
|
command,
|
||||||
timeout,
|
timeout,
|
||||||
@@ -850,7 +848,7 @@ impl ExtensionManager {
|
|||||||
&effective_working_dir,
|
&effective_working_dir,
|
||||||
container.map(|c| c.id().to_string()),
|
container.map(|c| c.id().to_string()),
|
||||||
self.client_name.clone(),
|
self.client_name.clone(),
|
||||||
capabilities,
|
self.mcp_client_capabilities(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
Box::new(client)
|
Box::new(client)
|
||||||
@@ -875,10 +873,6 @@ impl ExtensionManager {
|
|||||||
command.arg("python").arg(file_path.to_str().unwrap());
|
command.arg("python").arg(file_path.to_str().unwrap());
|
||||||
});
|
});
|
||||||
|
|
||||||
let capabilities = GooseMcpClientCapabilities {
|
|
||||||
mcpui: self.capabilities.mcpui,
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = child_process_client(
|
let client = child_process_client(
|
||||||
command,
|
command,
|
||||||
timeout,
|
timeout,
|
||||||
@@ -886,7 +880,7 @@ impl ExtensionManager {
|
|||||||
&effective_working_dir,
|
&effective_working_dir,
|
||||||
container.map(|c| c.id().to_string()),
|
container.map(|c| c.id().to_string()),
|
||||||
self.client_name.clone(),
|
self.client_name.clone(),
|
||||||
capabilities,
|
self.mcp_client_capabilities(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,34 @@ pub type BoxError = Box<dyn std::error::Error + Sync + Send>;
|
|||||||
|
|
||||||
pub type Error = rmcp::ServiceError;
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait McpClientTrait: Send + Sync {
|
pub trait McpClientTrait: Send + Sync {
|
||||||
async fn list_tools(
|
async fn list_tools(
|
||||||
@@ -164,6 +192,40 @@ impl GooseClient {
|
|||||||
.and_then(|(_, value)| value.as_str())
|
.and_then(|(_, value)| value.as_str())
|
||||||
.map(|value| value.to_string())
|
.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 {
|
fn working_dir_roots(dir: &std::path::Path) -> ListRootsResult {
|
||||||
@@ -340,21 +402,7 @@ impl ClientHandler for GooseClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_info(&self) -> ClientInfo {
|
fn get_info(&self) -> ClientInfo {
|
||||||
let mut extensions = ExtensionCapabilities::new();
|
let extensions = self.resolved_extensions();
|
||||||
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InitializeRequestParams::new(
|
InitializeRequestParams::new(
|
||||||
ClientCapabilities::builder()
|
ClientCapabilities::builder()
|
||||||
@@ -363,11 +411,7 @@ impl ClientHandler for GooseClient {
|
|||||||
.enable_sampling()
|
.enable_sampling()
|
||||||
.enable_elicitation()
|
.enable_elicitation()
|
||||||
.build(),
|
.build(),
|
||||||
Implementation::new(
|
self.resolved_client_info(),
|
||||||
self.client_name.clone(),
|
|
||||||
std::env::var("GOOSE_MCP_CLIENT_VERSION")
|
|
||||||
.unwrap_or(env!("CARGO_PKG_VERSION").to_owned()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.with_protocol_version(ProtocolVersion::V_2025_03_26)
|
.with_protocol_version(ProtocolVersion::V_2025_03_26)
|
||||||
}
|
}
|
||||||
@@ -376,6 +420,7 @@ impl ClientHandler for GooseClient {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GooseMcpClientCapabilities {
|
pub struct GooseMcpClientCapabilities {
|
||||||
pub mcpui: bool,
|
pub mcpui: bool,
|
||||||
|
pub host_info: Option<GooseMcpHostInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The MCP client is the interface for MCP operations.
|
/// The MCP client is the interface for MCP operations.
|
||||||
@@ -769,8 +814,14 @@ mod tests {
|
|||||||
|
|
||||||
fn new_client(platform: GoosePlatform) -> GooseClient {
|
fn new_client(platform: GoosePlatform) -> GooseClient {
|
||||||
let capabilities = match platform {
|
let capabilities = match platform {
|
||||||
GoosePlatform::GooseDesktop => GooseMcpClientCapabilities { mcpui: true },
|
GoosePlatform::GooseDesktop => GooseMcpClientCapabilities {
|
||||||
GoosePlatform::GooseCli => GooseMcpClientCapabilities { mcpui: false },
|
mcpui: true,
|
||||||
|
host_info: None,
|
||||||
|
},
|
||||||
|
GoosePlatform::GooseCli => GooseMcpClientCapabilities {
|
||||||
|
mcpui: false,
|
||||||
|
host_info: None,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
GooseClient::new(
|
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]
|
#[test]
|
||||||
fn test_working_dir_roots_returns_current_dir_as_root() {
|
fn test_working_dir_roots_returns_current_dir_as_root() {
|
||||||
let dir = PathBuf::from("/tmp/test-project");
|
let dir = PathBuf::from("/tmp/test-project");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use async_trait::async_trait;
|
|||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
use goose::acp::server::{serve, AcpProviderFactory, GooseAcpAgent};
|
use goose::acp::server::{serve, AcpProviderFactory, GooseAcpAgent};
|
||||||
pub use goose::acp::{map_permission_response, PermissionDecision};
|
pub use goose::acp::{map_permission_response, PermissionDecision};
|
||||||
|
use goose::agents::GoosePlatform;
|
||||||
use goose::builtin_extension::register_builtin_extensions;
|
use goose::builtin_extension::register_builtin_extensions;
|
||||||
use goose::config::paths::Paths;
|
use goose::config::paths::Paths;
|
||||||
use goose::config::{GooseMode, PermissionManager};
|
use goose::config::{GooseMode, PermissionManager};
|
||||||
@@ -190,6 +191,7 @@ pub async fn spawn_acp_server_in_process(
|
|||||||
data_root.to_path_buf(),
|
data_root.to_path_buf(),
|
||||||
goose_mode,
|
goose_mode,
|
||||||
true,
|
true,
|
||||||
|
GoosePlatform::GooseCli,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
@@ -261,7 +261,10 @@ async fn test_replayed_session(
|
|||||||
provider,
|
provider,
|
||||||
session_manager,
|
session_manager,
|
||||||
GoosePlatform::GooseDesktop.to_string(),
|
GoosePlatform::GooseDesktop.to_string(),
|
||||||
ExtensionManagerCapabilities { mcpui: true },
|
ExtensionManagerCapabilities {
|
||||||
|
mcpui: true,
|
||||||
|
host_info: None,
|
||||||
|
},
|
||||||
));
|
));
|
||||||
|
|
||||||
#[allow(clippy::redundant_closure_call)]
|
#[allow(clippy::redundant_closure_call)]
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
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 {
|
import {
|
||||||
PROTOCOL_VERSION,
|
PROTOCOL_VERSION,
|
||||||
type Client,
|
type Client,
|
||||||
@@ -7,6 +11,7 @@ import {
|
|||||||
type RequestPermissionRequest,
|
type RequestPermissionRequest,
|
||||||
type RequestPermissionResponse,
|
type RequestPermissionResponse,
|
||||||
} from "@agentclientprotocol/sdk";
|
} from "@agentclientprotocol/sdk";
|
||||||
|
import packageJson from "../../../package.json";
|
||||||
import { createWebSocketStream } from "./createWebSocketStream";
|
import { createWebSocketStream } from "./createWebSocketStream";
|
||||||
import { perfLog } from "@/shared/lib/perfLog";
|
import { perfLog } from "@/shared/lib/perfLog";
|
||||||
|
|
||||||
@@ -81,12 +86,18 @@ async function initializeConnection(): Promise<GooseClient> {
|
|||||||
const tInit = performance.now();
|
const tInit = performance.now();
|
||||||
await client.initialize({
|
await client.initialize({
|
||||||
protocolVersion: PROTOCOL_VERSION,
|
protocolVersion: PROTOCOL_VERSION,
|
||||||
clientCapabilities: {},
|
clientCapabilities: {
|
||||||
clientInfo: {
|
_meta: {
|
||||||
name: "goose2",
|
goose: {
|
||||||
version: "0.1.0",
|
mcpHostCapabilities: DEFAULT_GOOSE_MCP_HOST_CAPABILITIES,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
clientInfo: {
|
||||||
|
name: packageJson.name,
|
||||||
|
version: packageJson.version,
|
||||||
|
},
|
||||||
|
} satisfies GooseInitializeRequest);
|
||||||
perfLog(
|
perfLog(
|
||||||
`[perf:conn] client.initialize in ${(performance.now() - tInit).toFixed(1)}ms (total ${(performance.now() - tStart).toFixed(1)}ms)`,
|
`[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:
|
sdk:
|
||||||
dependencies:
|
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:
|
zod:
|
||||||
specifier: ^3.25.76
|
specifier: ^3.25.76
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/ext-apps": "^0.3.1",
|
||||||
"zod": "^3.25.76"
|
"zod": "^3.25.76"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from "./generated/types.gen.js";
|
|||||||
export * from "./generated/zod.gen.js";
|
export * from "./generated/zod.gen.js";
|
||||||
export { GooseClient } from "./goose-client.js";
|
export { GooseClient } from "./goose-client.js";
|
||||||
export { createHttpStream } from "./http-stream.js";
|
export { createHttpStream } from "./http-stream.js";
|
||||||
|
export * from "./mcp-apps.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ClientSideConnection,
|
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