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}