From 6f687aabd2adabd4197a501a75b961dba6b02cf2 Mon Sep 17 00:00:00 2001 From: Coffee Date: Thu, 19 Mar 2026 12:58:45 +0800 Subject: [PATCH] Restrict MCP runtime auth to the NEAR AI companion --- src/cli/mcp.rs | 4 ++-- src/tools/mcp/config.rs | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/cli/mcp.rs b/src/cli/mcp.rs index 45c354ed..d9c87aeb 100644 --- a/src/cli/mcp.rs +++ b/src/cli/mcp.rs @@ -11,7 +11,6 @@ use clap::{Args, Subcommand}; use crate::config::{Config, LlmConfig}; use crate::db::Database; use crate::secrets::SecretsStore; -use crate::settings::Settings; use crate::tools::mcp::{ McpClient, McpProcessManager, McpServerConfig, McpSessionManager, OAuthConfig, auth::{authorize_mcp_server, is_authenticated}, @@ -728,7 +727,8 @@ async fn get_secrets_store() -> anyhow::Result Result { - LlmConfig::resolve(&Settings::default()) + let settings = crate::config::load_bootstrap_settings(None)?; + LlmConfig::resolve(&settings) } #[cfg(test)] diff --git a/src/tools/mcp/config.rs b/src/tools/mcp/config.rs index 92faea9d..eb1a7daf 100644 --- a/src/tools/mcp/config.rs +++ b/src/tools/mcp/config.rs @@ -55,7 +55,10 @@ pub struct McpServerConfig { /// /// This is used for companion MCP servers that should reuse an existing /// provider identity instead of running their own MCP OAuth flow. - #[serde(default, skip_serializing_if = "Option::is_none")] + /// + /// Security: this field is runtime-only. Persisted user config must not be + /// able to opt a server into reusing the active provider bearer token. + #[serde(default, skip_serializing, skip_deserializing)] pub auth_source: Option, /// Whether this server is enabled. @@ -178,6 +181,15 @@ impl McpServerConfig { }); } + if self.uses_runtime_auth_source() && !is_nearai_companion_server_name(&self.name) { + return Err(ConfigError::InvalidConfig { + reason: format!( + "Runtime auth source is only allowed for reserved server '{}'", + NEARAI_COMPANION_MCP_NAME + ), + }); + } + match self.effective_transport() { EffectiveTransport::Http => { if self.url.is_empty() { @@ -872,6 +884,19 @@ mod tests { ); } + #[test] + fn test_deserialize_ignores_persisted_auth_source() { + let raw = serde_json::json!({ + "name": "user-managed", + "url": "https://mcp.example.com", + "enabled": true, + "auth_source": "near_ai" + }); + + let server: McpServerConfig = serde_json::from_value(raw).expect("server"); + assert_eq!(server.auth_source, None); + } + #[cfg(feature = "libsql")] #[test] fn test_derive_nearai_companion_mcp_server_strips_trailing_v1() { @@ -934,6 +959,20 @@ mod tests { assert!(config.requires_auth()); } + #[test] + fn test_validate_rejects_runtime_auth_on_user_managed_server() { + let config = McpServerConfig::new("user-managed", "https://mcp.example.com") + .with_auth_source(McpAuthSource::NearAi); + + let err = config + .validate() + .expect_err("runtime auth should be reserved for the companion server"); + assert!( + err.to_string().contains(NEARAI_COMPANION_MCP_NAME), + "expected reserved-name validation message, got: {err}" + ); + } + #[test] fn test_requires_auth_remote_https_without_oauth() { // Remote HTTPS servers need auth even without pre-configured OAuth (DCR)