Skip to main content

cosh_tools/web/
mod.rs

1//! Shared-state wrapper for web tool operations.
2//!
3//! [`Web`] holds configuration — such as result count — so callers don't
4//! have to construct [`WebFetch`] / [`WebSearch`] on every invocation.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use cosh_tools::web::Web;
10//!
11//! let web = Web::new().num_results(5);
12//! web.fetch("https://example.com").await;
13//! web.search("rust programming").await;
14//! ```
15
16pub mod fetch;
17pub mod search;
18#[cfg(test)]
19mod test;
20pub mod types;
21
22pub use fetch::{WebFetch, fetch};
23pub use search::{WebSearch, search};
24pub use types::{WebFetchInput, WebSearchInput};
25
26use crate::ToolDescription;
27
28/// Shared-state wrapper for web tool operations.
29///
30/// Use the builder method [`num_results`](Self::num_results) after
31/// [`new`](Self::new) to configure search result count, then call the
32/// operation methods directly.
33pub struct Web {
34    num_results: u32,
35
36    /// MCP Tool description for `fetch`.
37    pub description_fetch: ToolDescription,
38    /// MCP Tool description for `search`.
39    pub description_search: ToolDescription,
40}
41
42impl Default for Web {
43    fn default() -> Self {
44        Self::new()
45    }
46}
47
48impl Web {
49    /// Create a new `Web` with default search result count (10).
50    #[must_use]
51    pub fn new() -> Self {
52        Self {
53            num_results: 10,
54            description_fetch: serde_json::json!({
55                "name": "web_fetch",
56                "description": concat!(
57                    "Fetch a URL and return its content as clean, readable markdown. ",
58                    "Strips navigation, scripts, and boilerplate HTML to produce ",
59                    "LLM-friendly text."
60                ),
61                "inputSchema": {
62                    "type": "object",
63                    "properties": {
64                        "url": {
65                            "type": "string",
66                            "description": "The full HTTP or HTTPS URL to fetch"
67                        }
68                    },
69                    "required": ["url"]
70                }
71            }),
72            description_search: serde_json::json!({
73                "name": "web_search",
74                "description": concat!(
75                    "Search the web using a text query and return results as clean ",
76                    "markdown. Each result includes a title, snippet, and URL. ",
77                    "Use this to find current information, documentation, or ",
78                    "answers that are not available in the local codebase."
79                ),
80                "inputSchema": {
81                    "type": "object",
82                    "properties": {
83                        "query": {
84                            "type": "string",
85                            "description": "The search query string"
86                        }
87                    },
88                    "required": ["query"]
89                }
90            }),
91        }
92    }
93
94    /// Set the number of search results (capped at 10 by the underlying API).
95    #[must_use]
96    pub const fn num_results(mut self, n: u32) -> Self {
97        self.num_results = n;
98        self
99    }
100
101    /// Fetch a URL, returning clean markdown for LLM context.
102    ///
103    /// See [`fetch`] for details.
104    ///
105    /// # Errors
106    ///
107    /// Returns `Err` if the fetch fails or all fallback methods are exhausted.
108    pub async fn fetch(&self, fetch: WebFetch) -> Result<String, String> {
109        fetch::fetch(&fetch).await
110    }
111
112    /// Search the web, returning clean markdown for LLM context.
113    ///
114    /// See [`search`] for details.
115    ///
116    /// # Errors
117    ///
118    /// Returns `Err` if the query is empty, validation fails, or the search
119    /// itself fails.
120    pub async fn search(&self, query: &str) -> Result<String, String> {
121        search(&WebSearch {
122            num_results: self.num_results,
123            query: query.to_string(),
124        })
125        .await
126    }
127}