feat: Created the --auth-mcp CLI flag to let users auth with remote MCP servers without needing to be in the REPL
This commit is contained in:
+29
-1
@@ -5,9 +5,9 @@ use crate::utils::list_file_names;
|
|||||||
use crate::vault::Vault;
|
use crate::vault::Vault;
|
||||||
use clap_complete::{CompletionCandidate, Shell, generate};
|
use clap_complete::{CompletionCandidate, Shell, generate};
|
||||||
use clap_complete_nushell::Nushell;
|
use clap_complete_nushell::Nushell;
|
||||||
use std::env;
|
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::{env, fs};
|
||||||
|
|
||||||
const COYOTE_CLI_NAME: &str = "coyote";
|
const COYOTE_CLI_NAME: &str = "coyote";
|
||||||
|
|
||||||
@@ -134,6 +134,34 @@ pub(super) fn session_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn mcp_server_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
|
let cur = current.to_string_lossy();
|
||||||
|
let content = match fs::read_to_string(paths::mcp_config_file()) {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
let json: serde_json::Value = match serde_json::from_str(&content) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return vec![],
|
||||||
|
};
|
||||||
|
let servers = match json.get("mcpServers").and_then(|v| v.as_object()) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return vec![],
|
||||||
|
};
|
||||||
|
|
||||||
|
servers
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, v)| {
|
||||||
|
v.get("type")
|
||||||
|
.and_then(|t| t.as_str())
|
||||||
|
.map(|t| t == "http" || t == "sse")
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.filter(|(k, _)| k.starts_with(&*cur))
|
||||||
|
.map(|(k, _)| CompletionCandidate::new(k))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn secrets_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
pub(super) fn secrets_completer(current: &OsStr) -> Vec<CompletionCandidate> {
|
||||||
let cur = current.to_string_lossy();
|
let cur = current.to_string_lossy();
|
||||||
match load_app_config_for_completion() {
|
match load_app_config_for_completion() {
|
||||||
|
|||||||
+5
-2
@@ -1,8 +1,8 @@
|
|||||||
mod completer;
|
mod completer;
|
||||||
|
|
||||||
use crate::cli::completer::{
|
use crate::cli::completer::{
|
||||||
ShellCompletion, agent_completer, macro_completer, model_completer, rag_completer,
|
ShellCompletion, agent_completer, macro_completer, mcp_server_completer, model_completer,
|
||||||
role_completer, secrets_completer, session_completer,
|
rag_completer, role_completer, secrets_completer, session_completer,
|
||||||
};
|
};
|
||||||
use crate::config::{AssetCategory, InstallFilter, MemoryScope};
|
use crate::config::{AssetCategory, InstallFilter, MemoryScope};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
@@ -171,6 +171,9 @@ pub struct Cli {
|
|||||||
/// Authenticate with an LLM provider using OAuth (e.g., --authenticate client_name)
|
/// Authenticate with an LLM provider using OAuth (e.g., --authenticate client_name)
|
||||||
#[arg(long, exclusive = true, value_name = "CLIENT_NAME")]
|
#[arg(long, exclusive = true, value_name = "CLIENT_NAME")]
|
||||||
pub authenticate: Option<Option<String>>,
|
pub authenticate: Option<Option<String>>,
|
||||||
|
/// Authenticate with an OAuth-protected remote MCP server (e.g., --auth-mcp server_name)
|
||||||
|
#[arg(long, exclusive = true, value_name = "SERVER_NAME", add = ArgValueCompleter::new(mcp_server_completer))]
|
||||||
|
pub auth_mcp: Option<String>,
|
||||||
/// Generate static shell completion scripts
|
/// Generate static shell completion scripts
|
||||||
#[arg(long, value_name = "SHELL", value_enum)]
|
#[arg(long, value_name = "SHELL", value_enum)]
|
||||||
pub completions: Option<ShellCompletion>,
|
pub completions: Option<ShellCompletion>,
|
||||||
|
|||||||
+45
-2
@@ -28,11 +28,12 @@ use crate::config::{
|
|||||||
install_builtins, list_agents, load_env_file, macro_execute, sync_models,
|
install_builtins, list_agents, load_env_file, macro_execute, sync_models,
|
||||||
};
|
};
|
||||||
use crate::function::supervisor::{GuardrailAction, check_pending_agents_guardrail};
|
use crate::function::supervisor::{GuardrailAction, check_pending_agents_guardrail};
|
||||||
|
use crate::mcp::McpServersConfig;
|
||||||
use crate::render::{prompt_theme, render_error};
|
use crate::render::{prompt_theme, render_error};
|
||||||
use crate::repl::Repl;
|
use crate::repl::Repl;
|
||||||
use crate::utils::*;
|
use crate::utils::*;
|
||||||
use crate::vault::Vault;
|
use crate::vault::{Vault, interpolate_secrets};
|
||||||
use anyhow::{Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use clap::{CommandFactory, Parser};
|
use clap::{CommandFactory, Parser};
|
||||||
use clap_complete::CompleteEnv;
|
use clap_complete::CompleteEnv;
|
||||||
use client::ClientConfig;
|
use client::ClientConfig;
|
||||||
@@ -120,6 +121,48 @@ async fn main() -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(server_name) = &cli.auth_mcp {
|
||||||
|
let cfg = Config::load_with_interpolation(true).await?;
|
||||||
|
let app_config = AppConfig::from_config(cfg)?;
|
||||||
|
let vault = Vault::init(&app_config)?;
|
||||||
|
let mcp_path = paths::mcp_config_file();
|
||||||
|
if !mcp_path.exists() {
|
||||||
|
bail!(
|
||||||
|
"No MCP configuration file found at '{}'",
|
||||||
|
mcp_path.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw = tokio::fs::read_to_string(&mcp_path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Failed to read MCP config at '{}'", mcp_path.display()))?;
|
||||||
|
|
||||||
|
let (content, missing) = interpolate_secrets(&raw, &vault)?;
|
||||||
|
if !missing.is_empty() {
|
||||||
|
bail!(
|
||||||
|
"MCP config references vault secrets that are missing: {:?}",
|
||||||
|
missing
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mcp_config: McpServersConfig =
|
||||||
|
serde_json::from_str(&content).context("Failed to parse MCP config file")?;
|
||||||
|
let spec = mcp_config
|
||||||
|
.mcp_servers
|
||||||
|
.get(server_name.as_str())
|
||||||
|
.ok_or_else(|| anyhow!("MCP server '{server_name}' not found in mcp.json"))?;
|
||||||
|
if !spec.is_remote() {
|
||||||
|
bail!(
|
||||||
|
"MCP server '{server_name}' is a stdio server; OAuth is only supported for http/sse servers"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = spec.url.as_deref().expect("validated: remote spec has url");
|
||||||
|
mcp::oauth::run_mcp_oauth_flow(server_name, url, spec.oauth_client_id.as_deref()).await?;
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if vault_flags {
|
if vault_flags {
|
||||||
let cfg = Config::load_with_interpolation(true).await?;
|
let cfg = Config::load_with_interpolation(true).await?;
|
||||||
let app_config = AppConfig::from_config(cfg)?;
|
let app_config = AppConfig::from_config(cfg)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user