F6: kimi-cli command parity — kigi acp, mcp auth, --mcp-config-file

- `kigi acp`: top-level alias for the stdio ACP server (kimi-cli `acp`).
  AgentArgs.mode is now optional — bare `kigi agent` and `kigi acp` both
  default to stdio at dispatch; `kigi acp <mode>` is rejected.
- `kigi mcp auth <name>`: authorize an OAuth-enabled remote MCP server
  (kimi-cli `mcp auth`). Reuses the doctor's interactive connection path:
  starts the named server with OauthInteractivity::Interactive (browser
  flow when required), completes the handshake, and reports the tool
  count. Stdio servers and unknown names fail with actionable errors.
- `--mcp-config-file <PATH>` (repeatable, global): extra MCP config files
  in the .mcp.json shape ({"mcpServers": {...}}), kimi-cli semantics.
  Files are validated at parse time (fail fast on unreadable/invalid
  JSON), carried across the TUI -> shell boundary via
  KIGI_MCP_CONFIG_FILES, and merged at HIGHEST priority — an explicitly
  passed file overrides every config scope. Covered by loader unit tests
  and verified live: an injected server surfaces in the session's
  x.ai/mcp/servers_updated notification via both the flag and the env.

The stale bare-agent-requires-mode CLI test is re-contracted to the new
default-to-stdio behavior.
This commit is contained in:
2026-07-17 20:15:56 -04:00
parent 74b210535e
commit a3f062b522
6 changed files with 246 additions and 12 deletions
+38 -4
View File
@@ -896,8 +896,9 @@ async fn run_agent_command(
drop(kigi_shell::agent::models::start_early_prefetch(None));
kigi_shell::agent::mvp_agent::warm_async_http_client();
tokio::task::spawn_blocking(|| {});
let is_stdio = matches!(agent_args.mode, AgentCmd::Stdio);
let is_leader = matches!(agent_args.mode, AgentCmd::Leader(_));
let mode = agent_args.mode.clone().unwrap_or(AgentCmd::Stdio);
let is_stdio = matches!(mode, AgentCmd::Stdio);
let is_leader = matches!(mode, AgentCmd::Leader(_));
if !is_stdio && !is_leader {
eprintln!(
"Kigi v{}",
@@ -965,7 +966,7 @@ async fn run_agent_command(
storage_mode: None,
});
let agent_memory_config = agent_config.memory_config.clone();
let leader_eligible = matches!(&agent_args.mode, AgentCmd::Stdio);
let leader_eligible = is_stdio;
let (use_leader, policy_disable_reason) = resolve_use_leader(
agent_args.leader,
agent_args.no_leader,
@@ -1128,7 +1129,7 @@ async fn run_agent_command(
}
}
}
match agent_args.mode {
match mode {
AgentCmd::Stdio => run_stdio_agent(&agent_config, None, agent_memory_config).await,
AgentCmd::Serve(a) => {
let secret = a.get_secret();
@@ -1439,6 +1440,21 @@ async fn async_main() -> Result<()> {
if let Some(ref socket) = args.leader_socket {
unsafe { std::env::set_var(kigi_shell::leader::LEADER_SOCKET_ENV, socket) };
}
if !args.mcp_config_file.is_empty() {
// Fail fast on unusable files: an explicitly passed config that
// cannot be read must abort, not silently degrade the session.
for path in &args.mcp_config_file {
let content = std::fs::read_to_string(path)
.map_err(|e| anyhow::anyhow!("--mcp-config-file {}: {e}", path.display()))?;
serde_json::from_str::<serde_json::Value>(&content).map_err(|e| {
anyhow::anyhow!("--mcp-config-file {}: invalid JSON: {e}", path.display())
})?;
}
let joined = std::env::join_paths(&args.mcp_config_file).map_err(|e| {
anyhow::anyhow!("--mcp-config-file paths cannot be joined into the env: {e}")
})?;
unsafe { std::env::set_var(kigi_shell::util::config::MCP_CONFIG_FILES_ENV, joined) };
}
if let Some(ref path) = args.debug_file {
unsafe {
std::env::set_var("KIGI_DEBUG_LOG", path);
@@ -1505,6 +1521,24 @@ async fn async_main() -> Result<()> {
}
return Ok(());
}
Command::Acp(agent_args) => {
if let Some(mode) = &agent_args.mode {
anyhow::bail!(
"`kigi acp` always runs the stdio server; `{mode:?}` does not apply. \
Use `kigi agent ...` for other runtime modes."
);
}
enforce_minimum_version_or_exit(&update_config).await;
return run_agent_command(
agent_args,
args.permission_mode_flag.clone(),
args.trust,
args.no_auto_update,
args.disable_web_search,
&update_config,
)
.await;
}
Command::Agent(agent_args) => {
if args.leader || args.no_leader {
let flag = if args.leader {