use anyhow::{anyhow, Result};
use eggsearch::core::config::{AppConfig, Mode};
use eggsearch::core::WebSearchRequest;
use eggsearch::mcp::ServerState;
use std::sync::Arc;
pub async fn run(
cfg: &AppConfig,
query: &str,
max_results: usize,
as_json: bool,
providers: &[String],
) -> Result<()> {
if cfg.search.mode == Mode::Off {
anyhow::bail!("search is disabled by policy; set [search].mode = \"live\" to enable");
}
let state = Arc::new(ServerState::build(cfg.clone())?);
let effective_providers = cfg
.resolve_providers(providers)
.map_err(|e| anyhow!("{}", e))?;
let (_, unknown) = state.adapter.select_engines(&effective_providers);
if !unknown.is_empty() {
anyhow::bail!("unknown provider id(s): {}", unknown.join(", "));
}
let req = WebSearchRequest {
query: query.to_string(),
max_results: Some(max_results),
providers: effective_providers,
safe_search: None,
timeout_ms: None,
};
if let Err(e) = req.validate(cfg.search.max_query_chars) {
return Err(anyhow!("invalid query: {e}"));
}
let resolution = eggsearch::core::query::resolve_max_results(
req.max_results,
cfg.search.default_max_results,
cfg.search.max_results_cap,
);
let resp = state.adapter.web_search(&req, resolution.effective).await;
if as_json {
let payload = serde_json::json!({
"query": resp.query,
"mode": resp.mode,
"results": resp.results,
"providers_queried": resp.providers_queried,
"providers_failed": resp.providers_failed,
"warnings": resp.warnings.iter().map(|w| format!("[{}] {}", w.provider_id, w.message)).collect::<Vec<_>>(),
});
println!("{}", serde_json::to_string_pretty(&payload)?);
} else {
println!(
"# Results for '{}' ({} items, {} failed)",
query,
resp.results.len(),
resp.providers_failed.len()
);
for (i, c) in resp.results.iter().enumerate() {
let snippet = c.snippet.as_deref().unwrap_or("").replace('\n', " ");
let providers = c.providers.join(", ");
println!(
"\n{}. {}\n {}\n [{}]\n {}",
i + 1,
c.title,
c.url,
providers,
snippet
);
}
if !resp.warnings.is_empty() {
println!("\nWarnings:");
for w in &resp.warnings {
println!(" - [{}] {}", w.provider_id, w.message);
}
}
if !resp.providers_failed.is_empty() {
println!("\nFailed providers:");
for f in &resp.providers_failed {
println!(" - {}: {} ({})", f.id, f.message, f.error_class);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use eggsearch::core::config::{AppConfig, Mode};
#[tokio::test]
async fn run_respects_mode_off() {
let mut cfg = AppConfig::default();
cfg.search.mode = Mode::Off;
let err = run(&cfg, "rust", 10, false, &[])
.await
.expect_err("expected policy denial");
assert!(err.to_string().contains("disabled by policy"), "got: {err}");
assert!(err.to_string().contains("[search].mode"), "got: {err}");
}
}