Skip to main content

web_retrieval/
lib.rs

1#![deny(warnings)]
2#![deny(clippy::all)]
3
4//! Web fetch and web search MCP tools.
5
6pub mod fetch;
7pub mod haiku;
8pub mod search;
9pub mod tools;
10pub mod types;
11
12use agentic_config::types::AnthropicServiceConfig;
13use agentic_config::types::ExaServiceConfig;
14use agentic_config::types::WebRetrievalConfig;
15use tokio::sync::OnceCell;
16
17/// Shared state container for web tools.
18///
19/// Wraps shared HTTP clients, lazy-initialized Anthropic client,
20/// and configuration for reuse across MCP calls.
21pub struct WebTools {
22    /// Shared HTTP client for fetching web pages
23    pub(crate) http: reqwest::Client,
24    /// Exa search API client
25    pub(crate) exa: exa_async::Client<exa_async::ExaConfig>,
26    /// Lazy-initialized Anthropic client for Haiku summarization
27    pub(crate) anthropic: OnceCell<anthropic_async::Client<anthropic_async::AnthropicConfig>>,
28    /// Web retrieval configuration (timeouts, limits, summarizer settings)
29    pub(crate) cfg: WebRetrievalConfig,
30    /// Anthropic service configuration (`base_url` for API endpoint override)
31    pub(crate) anthropic_cfg: AnthropicServiceConfig,
32}
33
34impl WebTools {
35    /// Create a new `WebTools` instance with custom configuration.
36    ///
37    /// # Panics
38    /// Panics if the reqwest HTTP client cannot be built.
39    #[must_use]
40    #[expect(
41        clippy::expect_used,
42        reason = "reqwest client build failure is rare (TLS/resolver init) and fatal; matches reqwest::Client::new() pattern"
43    )]
44    pub fn with_config(
45        cfg: WebRetrievalConfig,
46        exa_cfg: &ExaServiceConfig,
47        anthropic_cfg: AnthropicServiceConfig,
48    ) -> Self {
49        // Create Exa client with configured base_url
50        let exa_config = exa_async::ExaConfig::new().with_api_base(&exa_cfg.base_url);
51        Self {
52            http: reqwest::Client::builder()
53                .connect_timeout(std::time::Duration::from_secs(5))
54                .timeout(std::time::Duration::from_secs(cfg.request_timeout_secs))
55                .build()
56                .expect("reqwest client"),
57            exa: exa_async::Client::with_config(exa_config),
58            anthropic: OnceCell::new(),
59            cfg,
60            anthropic_cfg,
61        }
62    }
63
64    /// Create a new `WebTools` instance with default configuration.
65    ///
66    /// # Panics
67    /// Panics if the reqwest HTTP client cannot be built.
68    #[must_use]
69    pub fn new() -> Self {
70        Self::with_config(
71            WebRetrievalConfig::default(),
72            &ExaServiceConfig::default(),
73            AnthropicServiceConfig::default(),
74        )
75    }
76}
77
78impl Default for WebTools {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84/// Re-export the `build_registry` function and `WebTools` for registry consumers.
85pub use tools::build_registry;
86
87#[cfg(test)]
88impl WebTools {
89    /// Create a `WebTools` instance with a custom HTTP client for testing.
90    pub(crate) fn with_http_client(http: reqwest::Client) -> Self {
91        let exa_cfg = ExaServiceConfig::default();
92        let exa_config = exa_async::ExaConfig::new().with_api_base(&exa_cfg.base_url);
93        Self {
94            http,
95            exa: exa_async::Client::with_config(exa_config),
96            anthropic: OnceCell::new(),
97            cfg: WebRetrievalConfig::default(),
98            anthropic_cfg: AnthropicServiceConfig::default(),
99        }
100    }
101}