use clap::{Parser, Subcommand, ValueEnum};
use colored::Colorize;
use daedra::{
DaedraResult, SERVER_NAME, VERSION,
cache::CacheConfig,
server::{DaedraServer, ServerConfig, TransportType},
tools::{crawl_site, fetch, search},
types::{CrawlArgs, SafeSearchLevel, SearchArgs, SearchOptions, VisitPageArgs},
};
use std::time::Duration;
use tracing_subscriber::{EnvFilter, fmt};
#[derive(Parser, Debug)]
#[command(
name = "daedra",
version = VERSION,
author = "DIRMACS Global Services <build@dirmacs.com>",
about = "A high-performance web search and research MCP server",
long_about = "Daedra is a Model Context Protocol (MCP) server that provides web search and research capabilities.\n\n\
It can be used as:\n\
- An MCP server (STDIO or SSE transport)\n\
- A CLI tool for direct searches and page fetching\n\n\
For more information, visit: https://github.com/dirmacs/daedra"
)]
struct Cli {
#[arg(short, long, global = true)]
verbose: bool,
#[arg(short, long, global = true)]
quiet: bool,
#[arg(short, long, global = true, default_value = "pretty")]
format: OutputFormat,
#[arg(long, global = true)]
no_color: bool,
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
enum OutputFormat {
#[default]
Pretty,
Json,
JsonCompact,
}
#[derive(Subcommand, Debug)]
enum Commands {
Serve {
#[arg(short, long, default_value = "stdio")]
transport: TransportOption,
#[arg(short, long, default_value = "3000")]
port: u16,
#[arg(long, default_value = "127.0.0.1")]
host: String,
#[arg(long)]
no_cache: bool,
#[arg(long, default_value = "300")]
cache_ttl: u64,
},
Search {
query: String,
#[arg(short, long, default_value = "10")]
num_results: usize,
#[arg(short, long, default_value = "wt-wt")]
region: String,
#[arg(short, long, default_value = "moderate")]
safe_search: SafeSearchOption,
#[arg(short = 't', long)]
time_range: Option<String>,
},
Fetch {
url: String,
#[arg(short, long)]
selector: Option<String>,
#[arg(long)]
include_images: bool,
},
Crawl {
url: String,
#[arg(short, long, default_value = "25")]
max_pages: usize,
#[arg(short, long, default_value = "4")]
concurrency: usize,
},
Info,
Check,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
enum TransportOption {
#[default]
Stdio,
Sse,
}
#[derive(Debug, Clone, Copy, ValueEnum, Default)]
enum SafeSearchOption {
Off,
#[default]
Moderate,
Strict,
}
impl From<SafeSearchOption> for SafeSearchLevel {
fn from(opt: SafeSearchOption) -> Self {
match opt {
SafeSearchOption::Off => SafeSearchLevel::Off,
SafeSearchOption::Moderate => SafeSearchLevel::Moderate,
SafeSearchOption::Strict => SafeSearchLevel::Strict,
}
}
}
impl Commands {
async fn run(
self,
format: OutputFormat,
verbose: bool,
quiet: bool,
no_color: bool,
) -> DaedraResult<()> {
match self {
Commands::Serve {
transport,
port,
host,
no_cache,
cache_ttl,
} => {
if verbose
&& !quiet
&& !matches!(format, OutputFormat::Json | OutputFormat::JsonCompact)
&& matches!(transport, TransportOption::Sse)
{
print_banner();
}
run_serve(transport, port, host, no_cache, cache_ttl).await
},
Commands::Search {
query,
num_results,
region,
safe_search,
time_range,
} => {
run_search(
query,
num_results,
region,
safe_search,
time_range,
format,
no_color,
)
.await
},
Commands::Fetch {
url,
selector,
include_images,
} => run_fetch(url, selector, include_images, format, no_color).await,
Commands::Crawl {
url,
max_pages,
concurrency,
} => run_crawl(url, max_pages, concurrency, format, no_color).await,
Commands::Info => {
run_info(no_color);
Ok(())
},
Commands::Check => run_check(no_color).await,
}
}
}
struct CheckReporter {
no_color: bool,
}
impl CheckReporter {
fn new(no_color: bool) -> Self {
Self { no_color }
}
fn section(&self, title: &str) {
if self.no_color {
let message = match title {
"Configuration Check" => "
Checking Daedra configuration...",
"Connectivity Test" => "
Testing search functionality...",
_ => title,
};
println!("{message}");
} else {
print_section(title);
}
}
fn ok(&self, message: &str) {
if self.no_color {
println!(" [OK] {message}");
} else {
print_success(message);
}
}
fn fail(&self, message: &str) {
if self.no_color {
println!(" [FAIL] {message}");
} else {
print_error(message);
}
}
fn warn(&self, message: &str) {
if self.no_color {
println!(" [WARN] {message}");
} else {
println!(" {} {}", "⚠".yellow(), message.yellow());
}
}
fn backends(&self, backends: &[&str]) {
if self.no_color {
println!(" Backends: {}", backends.join(", "));
} else {
println!(
" {} {} backends: {}",
"✓".green(),
backends.len(),
backends.join(", ")
);
}
}
fn summary(&self, all_ok: bool) {
println!();
if all_ok {
if self.no_color {
println!("All checks passed!");
} else {
println!("{}", "✓ All checks passed!".green().bold());
}
} else if self.no_color {
println!("Some checks failed. See above for details.");
std::process::exit(1);
} else {
println!(
"{}",
"✗ Some checks failed. See above for details.".red().bold()
);
std::process::exit(1);
}
}
}
fn check_search_client(reporter: &CheckReporter) -> bool {
match search::SearchClient::new() {
Ok(_) => {
reporter.ok("Search client initialized");
true
}
Err(e) => {
reporter.fail(&format!("Search client: {e}"));
false
}
}
}
fn check_fetch_client(reporter: &CheckReporter) -> bool {
match fetch::FetchClient::new() {
Ok(_) => {
reporter.ok("Fetch client initialized");
true
}
Err(e) => {
reporter.fail(&format!("Fetch client: {e}"));
false
}
}
}
async fn check_search_connectivity(reporter: &CheckReporter) -> bool {
let test_args = SearchArgs {
query: "test".to_string(),
options: Some(SearchOptions {
num_results: 1,
..Default::default()
}),
};
let provider = daedra::tools::SearchProvider::auto();
let backends = provider.available_backends();
reporter.backends(&backends);
match provider.search(&test_args).await {
Ok(response) => {
if response.data.is_empty() {
reporter.warn("Search returned no results");
} else {
reporter.ok("Search connectivity verified");
}
true
}
Err(e) => {
reporter.fail(&format!("Search test: {e}"));
false
}
}
}
fn setup_logging(verbose: bool, use_stderr: bool, quiet: bool) {
let filter = if quiet {
EnvFilter::new("off")
} else if verbose {
EnvFilter::new("debug")
} else {
EnvFilter::new("info")
};
let subscriber = fmt()
.with_env_filter(filter)
.with_target(false)
.with_thread_ids(false);
if use_stderr {
subscriber.with_writer(std::io::stderr).init();
} else {
subscriber.init();
}
}
fn print_banner() {
println!(
r#"
{}
╔═══════════════════════════════════════════════════════════════╗
║ ║
║ {} ║
║ {} ║
║ ║
║ A high-performance web search and research MCP server ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
"#,
"".clear(),
format!("🔍 DAEDRA v{}", VERSION).bright_cyan().bold(),
"by DIRMACS Global Services".bright_black(),
);
}
fn print_success(message: &str) {
println!("{} {}", "✓".green().bold(), message);
}
fn print_error(message: &str) {
eprintln!("{} {}", "✗".red().bold(), message);
}
fn print_info(label: &str, value: &str) {
println!(" {} {}", format!("{}:", label).bright_blue(), value);
}
fn print_section(title: &str) {
println!("\n{}", title.yellow().bold());
println!("{}", "─".repeat(40).bright_black());
}
async fn run_serve(
transport: TransportOption,
port: u16,
host: String,
no_cache: bool,
cache_ttl: u64,
) -> DaedraResult<()> {
let cache_config = if no_cache {
CacheConfig {
enabled: false,
..Default::default()
}
} else {
CacheConfig {
ttl: Duration::from_secs(cache_ttl),
enabled: true,
..Default::default()
}
};
let config = ServerConfig {
cache: cache_config,
verbose: false,
..Default::default()
};
let server = DaedraServer::new(config)?;
let transport_type = match transport {
TransportOption::Stdio => TransportType::Stdio,
TransportOption::Sse => {
let host_parts: Vec<u8> = host.split('.').filter_map(|s| s.parse().ok()).collect();
if host_parts.len() != 4 {
return Err(daedra::types::DaedraError::InvalidArguments(
"Invalid host format".to_string(),
));
}
TransportType::Sse {
port,
host: [host_parts[0], host_parts[1], host_parts[2], host_parts[3]],
}
},
};
server.run(transport_type).await
}
async fn run_search(
query: String,
num_results: usize,
region: String,
safe_search: SafeSearchOption,
time_range: Option<String>,
format: OutputFormat,
no_color: bool,
) -> DaedraResult<()> {
let args = SearchArgs {
query: query.clone(),
options: Some(SearchOptions {
region,
safe_search: safe_search.into(),
num_results,
time_range,
}),
};
let provider = daedra::tools::SearchProvider::auto();
let response = provider.search(&args).await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&response)?);
},
OutputFormat::JsonCompact => {
println!("{}", serde_json::to_string(&response)?);
},
OutputFormat::Pretty => {
if no_color {
println!("\nSearch Results for: {}", query);
println!("{}", "=".repeat(50));
println!(
"Found {} results in region '{}'",
response.data.len(),
response.metadata.search_context.region
);
println!();
for (i, result) in response.data.iter().enumerate() {
println!("{}. {}", i + 1, result.title);
println!(" URL: {}", result.url);
println!(" {}", result.description);
println!(
" Source: {} | Type: {:?}",
result.metadata.source, result.metadata.content_type
);
println!();
}
} else {
print_section(&format!("Search Results for: {}", query.cyan()));
println!(
"Found {} results in region '{}'",
response.data.len().to_string().green(),
response.metadata.search_context.region.bright_blue()
);
println!();
for (i, result) in response.data.iter().enumerate() {
println!(
"{} {}",
format!("{}.", i + 1).bright_black(),
result.title.white().bold()
);
println!(
" {} {}",
"URL:".bright_black(),
result.url.bright_blue().underline()
);
println!(" {}", result.description.bright_white());
println!(
" {} {} {} {:?}",
"Source:".bright_black(),
result.metadata.source.yellow(),
"|".bright_black(),
result.metadata.content_type
);
println!();
}
}
},
}
Ok(())
}
async fn run_fetch(
url: String,
selector: Option<String>,
include_images: bool,
format: OutputFormat,
no_color: bool,
) -> DaedraResult<()> {
let args = VisitPageArgs {
url: url.clone(),
selector,
include_images,
};
let content = fetch::fetch_page(&args).await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&content)?);
},
OutputFormat::JsonCompact => {
println!("{}", serde_json::to_string(&content)?);
},
OutputFormat::Pretty => {
if no_color {
println!("\n{}", content.title);
println!("{}", "=".repeat(50));
println!("URL: {}", content.url);
println!("Fetched: {}", content.timestamp);
println!("Words: {}", content.word_count);
println!();
println!("{}", content.content);
if let Some(links) = content.links {
println!("\nLinks found ({}):", links.len());
for link in links.iter().take(10) {
println!(" - {} ({})", link.text, link.url);
}
}
} else {
print_section(&content.title.white().bold().to_string());
print_info("URL", &content.url.bright_blue().underline().to_string());
print_info("Fetched", &content.timestamp);
print_info("Words", &content.word_count.to_string().green().to_string());
println!();
println!("{}", content.content);
if let Some(links) = content.links {
print_section(&format!("Links found ({})", links.len()));
for link in links.iter().take(10) {
println!(
" {} {} {}",
"→".bright_black(),
link.text.white(),
format!("({})", link.url).bright_blue()
);
}
}
}
},
}
Ok(())
}
async fn run_crawl(
url: String,
max_pages: usize,
concurrency: usize,
format: OutputFormat,
no_color: bool,
) -> DaedraResult<()> {
let args = CrawlArgs {
root_url: url,
max_pages,
concurrency,
};
let result = crawl_site(args).await?;
match format {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&result)?);
},
OutputFormat::JsonCompact => {
println!("{}", serde_json::to_string(&result)?);
},
OutputFormat::Pretty => {
if no_color {
println!("\nCrawl complete: {} pages, {} errors",
result.summary.fetched, result.summary.failed);
for page in &result.pages {
println!("\n--- {} ---", page.url);
println!("{}", &page.markdown[..page.markdown.len().min(200)]);
}
} else {
print_section(&format!(
"Crawl complete: {} pages, {} errors",
result.summary.fetched.to_string().green(),
result.summary.failed.to_string().red()
));
for page in &result.pages {
println!("\n{} {}", "→".bright_black(), page.url.bright_blue());
println!(" {}", page.title.white().bold());
println!(" {}...", &page.markdown[..page.markdown.len().min(150)]);
}
}
},
}
Ok(())
}
fn run_info(no_color: bool) {
if no_color {
println!("\nDaedra Server Information");
println!("{}", "=".repeat(50));
println!(" Name: {}", SERVER_NAME);
println!(" Version: {}", VERSION);
println!(" Author: DIRMACS Global Services");
println!(" Repository: https://github.com/dirmacs/daedra");
println!();
println!("Available Tools:");
println!(" - search_duckduckgo: Search the web using DuckDuckGo");
println!(" - visit_page: Fetch and extract webpage content");
println!();
println!("Supported Transports:");
println!(" - stdio: Standard I/O for MCP clients");
println!(" - sse: Server-Sent Events over HTTP");
} else {
print_banner();
print_section("Server Information");
print_info("Name", SERVER_NAME);
print_info("Version", VERSION);
print_info("Author", "DIRMACS Global Services");
print_info("Repository", "https://github.com/dirmacs/daedra");
print_section("Available Tools");
println!(
" {} {}",
"search_duckduckgo".green(),
"- Search the web using DuckDuckGo".bright_black()
);
println!(
" {} {}",
"visit_page".green(),
"- Fetch and extract webpage content".bright_black()
);
print_section("Supported Transports");
println!(
" {} {}",
"stdio".cyan(),
"- Standard I/O for MCP clients".bright_black()
);
println!(
" {} {}",
"sse".cyan(),
"- Server-Sent Events over HTTP".bright_black()
);
}
}
async fn run_check(no_color: bool) -> DaedraResult<()> {
let reporter = CheckReporter::new(no_color);
reporter.section("Configuration Check");
let mut all_ok = check_search_client(&reporter);
all_ok &= check_fetch_client(&reporter);
reporter.section("Connectivity Test");
all_ok &= check_search_connectivity(&reporter).await;
reporter.summary(all_ok);
Ok(())
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
if cli.no_color {
colored::control::set_override(false);
}
if let Commands::Serve { transport, .. } = &cli.command {
let use_stderr = matches!(transport, TransportOption::Stdio);
setup_logging(cli.verbose, use_stderr, cli.quiet);
}
let result = cli
.command
.run(cli.format, cli.verbose, cli.quiet, cli.no_color)
.await;
if let Err(e) = result {
if cli.no_color {
eprintln!("Error: {}", e);
} else {
print_error(&e.to_string());
}
std::process::exit(1);
}
}