feat: Secret injection into the MCP configuration

This commit is contained in:
2025-10-15 16:06:59 -06:00
parent df8b326d89
commit 39fc863e22
5 changed files with 4410 additions and 4380 deletions
+3 -4
View File
@@ -9,8 +9,7 @@ use anyhow::{Context, Result};
use inquire::{validator::Validation, Text}; use inquire::{validator::Validation, Text};
use rust_embed::Embed; use rust_embed::Embed;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ffi::OsStr; use std::{ffi::OsStr, path::Path};
use std::{fs, fs::read_to_string, path::Path};
const DEFAULT_AGENT_NAME: &str = "rag"; const DEFAULT_AGENT_NAME: &str = "rag";
@@ -68,7 +67,7 @@ impl Agent {
#[cfg(unix)] #[cfg(unix)]
if is_script { if is_script {
use std::os::unix::fs::PermissionsExt; use std::{fs, os::unix::fs::PermissionsExt};
fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755))?; fs::set_permissions(&file_path, fs::Permissions::from_mode(0o755))?;
} }
} }
@@ -104,7 +103,7 @@ impl Agent {
McpRegistry::reinit(registry, mcp_servers, abort_signal.clone()).await?; McpRegistry::reinit(registry, mcp_servers, abort_signal.clone()).await?;
if !new_mcp_registry.is_empty() { if !new_mcp_registry.is_empty() {
functions.append_mcp_meta_functions(new_mcp_registry.list_servers()); functions.append_mcp_meta_functions(new_mcp_registry.list_started_servers());
} }
config.write().mcp_registry = Some(new_mcp_registry); config.write().mcp_registry = Some(new_mcp_registry);
+23 -25
View File
@@ -723,7 +723,7 @@ impl Config {
Ok(output) Ok(output)
} }
pub fn update(config: &GlobalConfig, data: &str) -> Result<()> { pub async fn update(config: &GlobalConfig, data: &str, abort_signal: AbortSignal) -> Result<()> {
let parts: Vec<&str> = data.split_whitespace().collect(); let parts: Vec<&str> = data.split_whitespace().collect();
if parts.len() != 2 { if parts.len() != 2 {
bail!("Usage: .set <key> <value>. If value is null, unset key."); bail!("Usage: .set <key> <value>. If value is null, unset key.");
@@ -745,7 +745,21 @@ impl Config {
} }
"use_mcp_servers" => { "use_mcp_servers" => {
let value = parse_value(value)?; let value = parse_value(value)?;
config.write().set_use_mcp_servers(value); config.write().set_use_mcp_servers(value.clone());
config.write().functions.clear_mcp_meta_functions();
let registry = config.write()
.mcp_registry
.take()
.expect("MCP registry should be initialized");
let new_mcp_registry =
McpRegistry::reinit(registry, value, abort_signal.clone()).await?;
if !new_mcp_registry.is_empty() {
config.write().functions
.append_mcp_meta_functions(new_mcp_registry.list_started_servers());
}
config.write().mcp_registry = Some(new_mcp_registry);
} }
"max_output_tokens" => { "max_output_tokens" => {
let value = parse_value(value)?; let value = parse_value(value)?;
@@ -1019,7 +1033,7 @@ impl Config {
if !new_mcp_registry.is_empty() { if !new_mcp_registry.is_empty() {
self.functions self.functions
.append_mcp_meta_functions(new_mcp_registry.list_servers()); .append_mcp_meta_functions(new_mcp_registry.list_started_servers());
} }
self.mcp_registry = Some(new_mcp_registry); self.mcp_registry = Some(new_mcp_registry);
@@ -2105,29 +2119,12 @@ impl Config {
if prefix.is_empty() { if prefix.is_empty() {
values.push("all".to_string()); values.push("all".to_string());
} }
if let Some(registry) = &self.mcp_registry {
values.extend( values.extend(
self.functions registry
.declarations() .list_configured_servers()
.iter()
.filter(|v| {
v.name.starts_with(MCP_INVOKE_META_FUNCTION_NAME_PREFIX)
|| v.name.starts_with(MCP_LIST_META_FUNCTION_NAME_PREFIX)
})
.map(|v| {
v.name
.strip_prefix(
format!("{MCP_LIST_META_FUNCTION_NAME_PREFIX}_").as_str(),
)
.or_else(|| {
v.name.strip_prefix(
format!("{MCP_INVOKE_META_FUNCTION_NAME_PREFIX}_")
.as_str(),
)
})
.unwrap()
.to_string()
}),
); );
}
values.extend(self.mapping_mcp_servers.keys().map(|v| v.to_string())); values.extend(self.mapping_mcp_servers.keys().map(|v| v.to_string()));
values values
.into_iter() .into_iter()
@@ -2698,12 +2695,13 @@ impl Config {
start_mcp_servers, start_mcp_servers,
self.use_mcp_servers.clone(), self.use_mcp_servers.clone(),
abort_signal.clone(), abort_signal.clone(),
self,
) )
.await?; .await?;
match mcp_registry.is_empty() { match mcp_registry.is_empty() {
false => { false => {
self.functions self.functions
.append_mcp_meta_functions(mcp_registry.list_servers()); .append_mcp_meta_functions(mcp_registry.list_started_servers());
} }
_ => debug!( _ => debug!(
"Skipping global MCP functions registration since start_mcp_servers was 'false'" "Skipping global MCP functions registration since start_mcp_servers was 'false'"
+32 -6
View File
@@ -14,8 +14,9 @@ use std::collections::{HashMap, HashSet};
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::path::PathBuf; use std::path::PathBuf;
use std::process::Stdio; use std::process::Stdio;
use std::sync::Arc; use std::sync::{Arc};
use tokio::process::Command; use tokio::process::Command;
use crate::vault::SECRET_RE;
pub const MCP_INVOKE_META_FUNCTION_NAME_PREFIX: &str = "mcp_invoke"; pub const MCP_INVOKE_META_FUNCTION_NAME_PREFIX: &str = "mcp_invoke";
pub const MCP_LIST_META_FUNCTION_NAME_PREFIX: &str = "mcp_list"; pub const MCP_LIST_META_FUNCTION_NAME_PREFIX: &str = "mcp_list";
@@ -57,6 +58,7 @@ impl McpRegistry {
start_mcp_servers: bool, start_mcp_servers: bool,
use_mcp_servers: Option<String>, use_mcp_servers: Option<String>,
abort_signal: AbortSignal, abort_signal: AbortSignal,
config: &Config,
) -> Result<Self> { ) -> Result<Self> {
let mut registry = Self { let mut registry = Self {
log_path, log_path,
@@ -83,7 +85,24 @@ impl McpRegistry {
let content = tokio::fs::read_to_string(Config::mcp_config_file()) let content = tokio::fs::read_to_string(Config::mcp_config_file())
.await .await
.with_context(err)?; .with_context(err)?;
let config: McpServersConfig = serde_json::from_str(&content).with_context(err)?; let mut missing_secrets = vec![];
let parsed_content = SECRET_RE.replace_all(&content, |caps: &fancy_regex::Captures<'_>| {
let secret = config.vault
.get_secret(&caps[1], false);
match secret {
Ok(s) => s,
Err(_) => {
missing_secrets.push(caps[1].to_string());
"".to_string()
}
}
});
if !missing_secrets.is_empty() {
return Err(anyhow!("MCP config file references secrets that are missing from the vault: {:?}", missing_secrets));
}
let config: McpServersConfig = serde_json::from_str(&parsed_content).with_context(err)?;
registry.config = Some(config); registry.config = Some(config);
if start_mcp_servers { if start_mcp_servers {
@@ -124,13 +143,12 @@ impl McpRegistry {
async fn start_select_mcp_servers(&mut self, use_mcp_servers: Option<String>) -> Result<()> { async fn start_select_mcp_servers(&mut self, use_mcp_servers: Option<String>) -> Result<()> {
if self.config.is_none() { if self.config.is_none() {
debug!("MCP config is not present; assuming MCP servers are disabled globally. skipping MCP initialization"); debug!("MCP config is not present; assuming MCP servers are disabled globally. Skipping MCP initialization");
return Ok(()); return Ok(());
} }
debug!("Starting selected MCP servers: {:?}", use_mcp_servers);
if let Some(servers) = use_mcp_servers { if let Some(servers) = use_mcp_servers {
debug!("Starting selected MCP servers: {:?}", servers);
let config = self let config = self
.config .config
.as_ref() .as_ref()
@@ -231,10 +249,18 @@ impl McpRegistry {
Ok(self) Ok(self)
} }
pub fn list_servers(&self) -> Vec<String> { pub fn list_started_servers(&self) -> Vec<String> {
self.servers.keys().cloned().collect() self.servers.keys().cloned().collect()
} }
pub fn list_configured_servers(&self) -> Vec<String> {
if let Some(config) = &self.config {
config.mcp_servers.keys().cloned().collect()
} else {
vec![]
}
}
pub fn catalog(&self) -> BoxFuture<'static, Result<Value>> { pub fn catalog(&self) -> BoxFuture<'static, Result<Value>> {
let servers: Vec<(String, Arc<ConnectedServer>)> = self let servers: Vec<(String, Arc<ConnectedServer>)> = self
.servers .servers
+6 -6
View File
@@ -184,12 +184,12 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| {
"Delete roles, sessions, RAGs, or agents", "Delete roles, sessions, RAGs, or agents",
AssertState::pass(), AssertState::pass(),
), ),
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
ReplCommand::new( ReplCommand::new(
".vault", ".vault",
"View or modify the Loki vault", "View or modify the Loki vault",
AssertState::pass(), AssertState::pass(),
), ),
ReplCommand::new(".exit", "Exit REPL", AssertState::pass()),
] ]
}); });
static COMMAND_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*(\.\S*)\s*").unwrap()); static COMMAND_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\s*(\.\S*)\s*").unwrap());
@@ -663,7 +663,7 @@ pub async fn run_repl_command(
} }
".set" => match args { ".set" => match args {
Some(args) => { Some(args) => {
Config::update(config, args)?; Config::update(config, args, abort_signal).await?;
} }
_ => { _ => {
println!("Usage: .set <key> <value>...") println!("Usage: .set <key> <value>...")
@@ -708,7 +708,7 @@ pub async fn run_repl_command(
config config
.write() .write()
.functions .functions
.append_mcp_meta_functions(registry.list_servers()); .append_mcp_meta_functions(registry.list_started_servers());
} }
config.write().mcp_registry = Some(registry); config.write().mcp_registry = Some(registry);
} }
@@ -730,7 +730,7 @@ pub async fn run_repl_command(
config config
.write() .write()
.functions .functions
.append_mcp_meta_functions(registry.list_servers()); .append_mcp_meta_functions(registry.list_started_servers());
} }
config.write().mcp_registry = Some(registry); config.write().mcp_registry = Some(registry);
} else { } else {
@@ -757,7 +757,7 @@ pub async fn run_repl_command(
config config
.write() .write()
.functions .functions
.append_mcp_meta_functions(registry.list_servers()); .append_mcp_meta_functions(registry.list_started_servers());
} }
config.write().mcp_registry = Some(registry); config.write().mcp_registry = Some(registry);
} }
@@ -782,7 +782,7 @@ pub async fn run_repl_command(
} }
Some(("get", name)) => { Some(("get", name)) => {
if let Some(name) = name { if let Some(name) = name {
config.read().vault.get_secret(name)?; config.read().vault.get_secret(name, true)?;
} else { } else {
println!("Usage: .vault get <name>"); println!("Usage: .vault get <name>");
} }
+11 -4
View File
@@ -1,14 +1,18 @@
mod utils; mod utils;
use std::sync::LazyLock;
use crate::cli::Cli; use crate::cli::Cli;
use crate::config::Config; use crate::config::Config;
use crate::vault::utils::ensure_password_file_initialized; use crate::vault::utils::ensure_password_file_initialized;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use fancy_regex::Regex;
use gman::providers::local::LocalProvider; use gman::providers::local::LocalProvider;
use gman::providers::SecretProvider; use gman::providers::SecretProvider;
use inquire::{required, Password, PasswordDisplayMode}; use inquire::{required, Password, PasswordDisplayMode};
use tokio::runtime::Handle; use tokio::runtime::Handle;
pub static SECRET_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{(.+)}}").unwrap());
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Vault { pub struct Vault {
local_provider: LocalProvider, local_provider: LocalProvider,
@@ -45,14 +49,17 @@ impl Vault {
Ok(()) Ok(())
} }
pub fn get_secret(&self, secret_name: &str) -> Result<()> { pub fn get_secret(&self, secret_name: &str, display_output: bool) -> Result<String> {
let h = Handle::current(); let h = Handle::current();
let secret = tokio::task::block_in_place(|| { let secret = tokio::task::block_in_place(|| {
h.block_on(self.local_provider.get_secret(secret_name)) h.block_on(self.local_provider.get_secret(secret_name))
})?; })?;
println!("{}", secret);
Ok(()) if display_output {
println!("{}", secret);
}
Ok(secret)
} }
pub fn update_secret(&self, secret_name: &str) -> Result<()> { pub fn update_secret(&self, secret_name: &str) -> Result<()> {
@@ -105,7 +112,7 @@ impl Vault {
} }
if let Some(secret_name) = cli.get_secret { if let Some(secret_name) = cli.get_secret {
config.vault.get_secret(&secret_name)?; config.vault.get_secret(&secret_name, true)?;
} }
if let Some(secret_name) = cli.update_secret { if let Some(secret_name) = cli.update_secret {