From 8bcdf1608f12a9be9e46023c241a17fb92a835c4 Mon Sep 17 00:00:00 2001 From: Coffee Date: Tue, 17 Mar 2026 14:04:16 +0800 Subject: [PATCH] Mark NEAR AI companion MCP as derived --- src/channels/web/handlers/extensions.rs | 1 + src/channels/web/server.rs | 3 +++ src/channels/web/types.rs | 3 +++ src/cli/mcp.rs | 28 ++++++++++++++++++++++--- src/extensions/manager.rs | 13 ++++++++++++ src/extensions/mod.rs | 8 +++++++ src/tools/mcp/config.rs | 4 ++++ 7 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/channels/web/handlers/extensions.rs b/src/channels/web/handlers/extensions.rs index 855fba3e..429dee13 100644 --- a/src/channels/web/handlers/extensions.rs +++ b/src/channels/web/handlers/extensions.rs @@ -68,6 +68,7 @@ pub async fn extensions_list_handler( tools: ext.tools, needs_setup: ext.needs_setup, has_auth: ext.has_auth, + derived: ext.derived, activation_status, activation_error: ext.activation_error, version: ext.version, diff --git a/src/channels/web/server.rs b/src/channels/web/server.rs index 34b1205c..5246d085 100644 --- a/src/channels/web/server.rs +++ b/src/channels/web/server.rs @@ -1885,6 +1885,7 @@ async fn extensions_list_handler( tools: ext.tools, needs_setup: ext.needs_setup, has_auth: ext.has_auth, + derived: ext.derived, activation_status, activation_error: ext.activation_error, version: ext.version, @@ -2795,6 +2796,7 @@ mod tests { tools: Vec::new(), needs_setup: true, has_auth: false, + derived: false, installed: true, activation_error: None, version: None, @@ -2832,6 +2834,7 @@ mod tests { tools: Vec::new(), needs_setup: true, has_auth: false, + derived: false, installed: true, activation_error: None, version: None, diff --git a/src/channels/web/types.rs b/src/channels/web/types.rs index 3fad9f35..70c07ba2 100644 --- a/src/channels/web/types.rs +++ b/src/channels/web/types.rs @@ -462,6 +462,9 @@ pub struct ExtensionInfo { /// Whether this extension has an auth configuration (OAuth or manual token). #[serde(default)] pub has_auth: bool, + /// Whether this extension is derived from runtime/provider state. + #[serde(default)] + pub derived: bool, /// WASM channel activation status. #[serde(skip_serializing_if = "Option::is_none")] pub activation_status: Option, diff --git a/src/cli/mcp.rs b/src/cli/mcp.rs index cc77e7ba..589acc54 100644 --- a/src/cli/mcp.rs +++ b/src/cli/mcp.rs @@ -281,6 +281,13 @@ async fn add_server(args: McpAddArgs) -> anyhow::Result<()> { /// Remove an MCP server. async fn remove_server(name: String) -> anyhow::Result<()> { + if config::is_nearai_companion_server_name(&name) { + anyhow::bail!( + "Server '{}' is derived from the active NEAR AI provider and cannot be removed directly", + name + ); + } + let db = connect_db().await; let mut servers = load_servers(db.as_deref()).await?; if !servers.remove(&name) { @@ -611,6 +618,13 @@ async fn test_server(name: String, user_id: String) -> anyhow::Result<()> { /// Toggle server enabled/disabled state. async fn toggle_server(name: String, enable: bool, disable: bool) -> anyhow::Result<()> { + if config::is_nearai_companion_server_name(&name) { + anyhow::bail!( + "Server '{}' is derived from the active NEAR AI provider and cannot be toggled directly", + name + ); + } + let db = connect_db().await; let mut servers = load_servers(db.as_deref()).await?; @@ -647,11 +661,19 @@ async fn connect_db() -> Option> { /// Load MCP servers (DB if available, else disk). async fn load_servers(db: Option<&dyn Database>) -> Result { - if let Some(db) = db { - config::load_mcp_servers_from_db(db, DEFAULT_USER_ID).await + let mut servers = if let Some(db) = db { + config::load_mcp_servers_from_db(db, DEFAULT_USER_ID).await? } else { - config::load_mcp_servers().await + config::load_mcp_servers().await? + }; + + if let Ok(cfg) = Config::from_env().await + && let Some(companion) = config::derive_nearai_companion_mcp_server(&cfg) + { + servers.insert_if_absent(companion); } + + Ok(servers) } /// Save MCP servers (DB if available, else disk). diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index 4d8f155f..c413668b 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -1055,6 +1055,9 @@ impl ExtensionManager { tools, needs_setup: false, has_auth: server.requires_auth(), + derived: crate::tools::mcp::config::is_nearai_companion_server_name( + &server.name, + ), installed: true, activation_error: None, version: None, @@ -1106,6 +1109,7 @@ impl ExtensionManager { tools: if active { vec![name] } else { Vec::new() }, needs_setup: auth_state == ToolAuthState::NeedsSetup, has_auth: auth_state != ToolAuthState::NoAuth, + derived: false, installed: true, activation_error: None, version, @@ -1162,6 +1166,7 @@ impl ExtensionManager { tools: Vec::new(), needs_setup: auth_state == ToolAuthState::NeedsSetup, has_auth: auth_state != ToolAuthState::NoAuth, + derived: false, installed: true, activation_error, version, @@ -1202,6 +1207,7 @@ impl ExtensionManager { tools: Vec::new(), needs_setup: false, has_auth: true, + derived: false, installed: true, activation_error: None, version: None, @@ -1236,6 +1242,7 @@ impl ExtensionManager { tools: Vec::new(), needs_setup: false, has_auth: false, + derived: false, installed: false, activation_error: None, version: entry.version, @@ -1266,6 +1273,12 @@ impl ExtensionManager { match kind { ExtensionKind::McpServer => { + if crate::tools::mcp::config::is_nearai_companion_server_name(name) { + return Err(ExtensionError::Config( + "This MCP server is derived from the active NEAR AI provider and cannot be removed directly".to_string(), + )); + } + // Unregister tools with this server's prefix let tool_names: Vec = self .tool_registry diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 2a4d189f..4a0284c2 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -504,6 +504,10 @@ pub struct InstalledExtension { /// Whether this extension has an auth configuration (OAuth or manual token). #[serde(default)] pub has_auth: bool, + /// Whether this extension is derived from provider/runtime state instead of + /// being a user-managed persisted configuration. + #[serde(default)] + pub derived: bool, /// Whether this extension is installed locally (false = available in registry but not installed). #[serde(default = "default_true")] pub installed: bool, @@ -934,6 +938,7 @@ mod tests { assert!(ext.installed, "installed should default to true"); assert!(!ext.needs_setup, "needs_setup should default to false"); assert!(!ext.has_auth); + assert!(!ext.derived); assert!(ext.tools.is_empty()); assert!(ext.display_name.is_none()); assert!(ext.description.is_none()); @@ -954,6 +959,7 @@ mod tests { tools: vec!["send_email".to_string(), "read_inbox".to_string()], needs_setup: true, has_auth: true, + derived: true, installed: false, activation_error: Some("token expired".to_string()), version: None, @@ -963,6 +969,7 @@ mod tests { assert_eq!(json["description"], "Read and send emails"); assert_eq!(json["url"], "https://gmail.example.com"); assert_eq!(json["needs_setup"], true); + assert_eq!(json["derived"], true); assert_eq!(json["installed"], false); assert_eq!(json["activation_error"], "token expired"); @@ -970,6 +977,7 @@ mod tests { assert_eq!(back.name, "gmail"); assert_eq!(back.tools.len(), 2); assert!(back.needs_setup); + assert!(back.derived); assert!(!back.installed); assert_eq!(back.activation_error.as_deref(), Some("token expired")); } diff --git a/src/tools/mcp/config.rs b/src/tools/mcp/config.rs index 7a6980ae..cc7912c6 100644 --- a/src/tools/mcp/config.rs +++ b/src/tools/mcp/config.rs @@ -292,6 +292,10 @@ impl McpServerConfig { /// Reserved name used for the companion chat-api MCP server derived from NEAR AI config. pub const NEARAI_COMPANION_MCP_NAME: &str = "_nearai_companion_mcp"; +pub fn is_nearai_companion_server_name(name: &str) -> bool { + name == NEARAI_COMPANION_MCP_NAME +} + /// Build the companion chat-api MCP server from the active NearAI config. /// /// The MCP endpoint is treated as a sibling to the versioned REST API: