use std::path::PathBuf;
use std::process::ExitCode;
use anyhow::{Context, Result};
use clap::{Parser, ValueEnum};
use dci_tool::{CorpusRoot, DciAgent, DciMcpService, Limits, SessionConfig};
use rig_core::client::{CompletionClient, ProviderClient};
use rig_core::completion::CompletionModel;
use rig_core::providers::{anthropic, ollama, openai};
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Provider {
Openai,
Anthropic,
Ollama,
}
#[derive(Debug, Parser)]
#[command(name = "dci-mcp", version, about)]
struct Cli {
#[arg(short, long, default_value = ".")]
corpus: PathBuf,
#[arg(short, long, value_enum, default_value_t = Provider::Openai)]
provider: Provider,
#[arg(short, long)]
model: Option<String>,
#[arg(long, default_value_t = dci_tool::DEFAULT_MAX_TURNS)]
max_turns: usize,
#[arg(long)]
no_gitignore: bool,
#[arg(long, default_value_t = SessionConfig::default().max_sessions)]
max_sessions: usize,
#[arg(long, default_value_t = SessionConfig::default().max_turns_per_session)]
max_turns_per_session: usize,
#[arg(long, default_value_t = SessionConfig::default().ttl.as_secs())]
session_ttl_secs: u64,
}
#[tokio::main]
async fn main() -> ExitCode {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.with_writer(std::io::stderr)
.init();
match run(Cli::parse()).await {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err:#}");
ExitCode::FAILURE
}
}
}
async fn run(cli: Cli) -> Result<()> {
let limits = Limits {
respect_gitignore: !cli.no_gitignore,
..Limits::default()
};
let corpus = CorpusRoot::with_limits(&cli.corpus, limits)
.with_context(|| format!("opening corpus at {}", cli.corpus.display()))?;
let session_config = SessionConfig {
max_sessions: cli.max_sessions,
max_turns_per_session: cli.max_turns_per_session,
ttl: std::time::Duration::from_secs(cli.session_ttl_secs),
};
let service = match cli.provider {
Provider::Openai => {
let client = openai::Client::from_env().context("initializing OpenAI client")?;
let model_id = cli.model.as_deref().unwrap_or(openai::GPT_4O).to_string();
let model = client.completion_model(&model_id);
build_service(model, corpus, cli.max_turns, None, &model_id, session_config)
}
Provider::Anthropic => {
let client = anthropic::Client::from_env().context("initializing Anthropic client")?;
let model_id = cli
.model
.as_deref()
.unwrap_or(anthropic::completion::CLAUDE_SONNET_4_6)
.to_string();
let model = client.completion_model(&model_id);
build_service(model, corpus, cli.max_turns, Some(4096), &model_id, session_config)
}
Provider::Ollama => {
let client = ollama::Client::from_env().context("initializing Ollama client")?;
let model_id = cli.model.as_deref().unwrap_or("llama3.1").to_string();
let model = client.completion_model(&model_id);
build_service(model, corpus, cli.max_turns, None, &model_id, session_config)
}
};
tracing::info!(
tool = service.tool_name(),
"serving DCI corpus over MCP stdio"
);
service
.serve_stdio()
.await
.map_err(|e| anyhow::anyhow!("MCP server error: {e}"))
}
fn build_service<M: CompletionModel + 'static>(
model: M,
corpus: CorpusRoot,
max_turns: usize,
max_tokens: Option<u64>,
model_label: &str,
session_config: SessionConfig,
) -> DciMcpService {
let mut builder = DciAgent::builder(model, corpus)
.max_turns(max_turns)
.model_label(model_label);
if let Some(max_tokens) = max_tokens {
builder = builder.max_tokens(max_tokens);
}
DciMcpService::new_with_config(builder.build(), session_config)
}