From 272047493a3590525043bdc52701e794ffd6f55c Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Mon, 23 Feb 2026 12:10:07 +0900 Subject: [PATCH 1/2] feat: add --verbose option for detailed search progress logging This commit adds a --verbose flag to search and fetch commands that provides detailed logging of the search process, including: - Search URLs being accessed - Page loading status - Pagination progress - Results count per page - Total patents collected Additionally, CI has been updated to test verbose mode functionality. Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/ci.yml | 13 +++++++++++ src/cli/mod.rs | 16 +++++++++++-- src/core/patent_search.rs | 48 +++++++++++++++++++++++++++++++++++++-- src/mcp/mod.rs | 2 +- 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca5b825..70d7df8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,3 +58,16 @@ jobs: - name: Check build run: cargo check --release + + - name: Test verbose mode + run: | + # Build the CLI first + cargo build --release + + # Test verbose mode with a simple search + ./target/release/google-patent-cli search --query "test" --limit 1 --verbose || { + echo "Verbose mode test failed" + exit 1 + } + + echo "Verbose mode test passed" diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 6fe60d2..299ff1f 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -57,6 +57,10 @@ pub struct SearchArgs { #[arg(long, default_value_t = false)] pub debug: bool, + /// Enable verbose output (shows detailed progress) + #[arg(long, default_value_t = false)] + pub verbose: bool, + /// Language/locale for patent pages (e.g., ja, en, zh) #[arg(long)] pub language: Option, @@ -79,6 +83,10 @@ pub struct FetchArgs { #[arg(long, default_value_t = false)] pub debug: bool, + /// Enable verbose output (shows detailed progress) + #[arg(long, default_value_t = false)] + pub verbose: bool, + /// Language/locale for patent pages (e.g., ja, en, zh) #[arg(long)] pub language: Option, @@ -133,7 +141,9 @@ pub async fn run_app(cli: Cli) -> Result<()> { } let config = Config::load()?; - let searcher = PatentSearcher::new(config.browser_path, !args.head, args.debug).await?; + let searcher = + PatentSearcher::new(config.browser_path, !args.head, args.debug, args.verbose) + .await?; let options = SearchOptions { query: args.query, @@ -152,7 +162,9 @@ pub async fn run_app(cli: Cli) -> Result<()> { } Commands::Fetch { args } => { let config = Config::load()?; - let searcher = PatentSearcher::new(config.browser_path, !args.head, args.debug).await?; + let searcher = + PatentSearcher::new(config.browser_path, !args.head, args.debug, args.verbose) + .await?; if args.raw { let html = searcher.get_raw_html(&args.patent_id, args.language.as_deref()).await?; diff --git a/src/core/patent_search.rs b/src/core/patent_search.rs index fce5f07..a306154 100644 --- a/src/core/patent_search.rs +++ b/src/core/patent_search.rs @@ -11,6 +11,7 @@ pub trait PatentSearch: Send + Sync { pub struct PatentSearcher { browser_manager: BrowserManager, + verbose: bool, } #[async_trait] @@ -57,10 +58,11 @@ impl PatentSearcher { browser_path: Option, headless: bool, debug: bool, + verbose: bool, ) -> Result { let browser_manager = BrowserManager::new(browser_path, headless, debug); - Ok(Self { browser_manager }) + Ok(Self { browser_manager, verbose }) } async fn search_internal(&self, options: &SearchOptions) -> Result { @@ -70,10 +72,20 @@ impl PatentSearcher { let base_url = options.to_url()?; + if self.verbose { + eprintln!("Search URL: {}", base_url); + } + if let Some(patent_number) = &options.patent_number { // Single patent lookup - no pagination needed + if self.verbose { + eprintln!("Fetching single patent: {}", patent_number); + } page.goto(&base_url).await?; + if self.verbose { + eprintln!("Waiting for page to load..."); + } // Wait for meta description or title tag to ensure page is loaded let loaded = page .wait_for_element("meta[name='description'], meta[name='DC.title']", 15) @@ -92,6 +104,9 @@ impl PatentSearcher { // Give a little time for all dynamic content (like claims) to fully render tokio::time::sleep(std::time::Duration::from_millis(1000)).await; + if self.verbose { + eprintln!("Extracting patent data..."); + } // Single patent page - extract structured data let result = page.evaluate(include_str!("scripts/extract_patent.js")).await?; @@ -113,6 +128,10 @@ impl PatentSearcher { let mut top_assignees: Option> = None; let mut top_cpcs: Option> = None; + if self.verbose { + eprintln!("Fetching search results (limit: {})...", limit); + } + // Append num=100 to base_url to fetch more results per page if needed // This reduces the need for multiple page loads for limits <= 100 let base_url = if limit > 10 { format!("{}&num=100", base_url) } else { base_url }; @@ -132,16 +151,27 @@ impl PatentSearcher { format!("{}&page={}", base_url, page_num) }; + if self.verbose { + eprintln!("Loading page {} of {}...", page_num + 1, pages_needed); + eprintln!("URL: {}", page_url); + } + page.goto(&page_url).await?; // Wait for results to load let loaded = page.wait_for_element(".search-result-item", 15).await?; if !loaded { // No results on this page, stop pagination + if self.verbose { + eprintln!("No results found on this page, stopping pagination."); + } let _ = page.close().await; break; } + if self.verbose { + eprintln!("Extracting search results from page..."); + } let results = page.evaluate(include_str!("scripts/extract_search_results.js")).await?; @@ -149,7 +179,10 @@ impl PatentSearcher { // Only capture total results and summary data from the first page if page_num == 0 { - total_results_str = sr.total_results; + total_results_str = sr.total_results.clone(); + if self.verbose { + eprintln!("Total results found: {}", total_results_str); + } top_assignees = sr.top_assignees; // Two-step CPC extraction: click CPCs tab and wait for DOM update @@ -162,6 +195,10 @@ impl PatentSearcher { let page_patents = sr.patents; + if self.verbose { + eprintln!("Found {} patents on this page", page_patents.len()); + } + // If we got no results, stop pagination if page_patents.is_empty() { break; @@ -176,8 +213,15 @@ impl PatentSearcher { } let _ = page.close().await; + if self.verbose { + eprintln!("Total patents collected: {}", all_patents.len()); + } + // Truncate to exact limit if all_patents.len() > limit { + if self.verbose { + eprintln!("Truncating to limit: {}", limit); + } all_patents.truncate(limit); } diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 26b37ab..81b7da3 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -154,7 +154,7 @@ impl ServerHandler for PatentHandler { /// Run the MCP server over stdio pub async fn run() -> anyhow::Result<()> { let config = Config::load()?; - let searcher = PatentSearcher::new(config.browser_path, true, false) + let searcher = PatentSearcher::new(config.browser_path, true, false, false) .await .map_err(|e| anyhow::anyhow!("Failed to create PatentSearcher: {}", e))?; let handler = PatentHandler::new(Arc::new(searcher)); From 090ede1ddcb5073bc0e0a9a2d2fe36bf5ede0f6d Mon Sep 17 00:00:00 2001 From: "Claude Sonnet 4.6" Date: Mon, 23 Feb 2026 14:51:22 +0900 Subject: [PATCH 2/2] fix: return proper MCP error responses from tools Previously, tool errors were returned as strings in the response content, which does not properly indicate an error condition to MCP clients. This commit updates the search_patents and fetch_patent tool methods to return Result types that map to MCP error responses with the isError field set according to the MCP specification. Changes: - Return Result instead of String from tool methods - Use ErrorData::new with proper error codes (INTERNAL_ERROR, INVALID_PARAMS) - Update tests to check for Ok/Err results instead of string content This ensures that MCP clients receive properly formatted error responses when tools encounter failures, following the MCP specification at: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#calling-tools Co-Authored-By: Claude Sonnet 4.5 --- src/mcp/mod.rs | 70 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/src/mcp/mod.rs b/src/mcp/mod.rs index 81b7da3..7e91be9 100644 --- a/src/mcp/mod.rs +++ b/src/mcp/mod.rs @@ -3,7 +3,9 @@ use crate::core::models::SearchOptions; use crate::core::patent_search::{PatentSearch, PatentSearcher}; use rmcp::{ handler::server::{tool::ToolRouter, wrapper::Parameters}, - model::{Implementation, ProtocolVersion, ServerCapabilities, ServerInfo, ToolsCapability}, + model::{ + ErrorCode, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo, ToolsCapability, + }, schemars::{self, JsonSchema}, service::{NotificationContext, RequestContext}, tool, tool_handler, tool_router, ErrorData, RoleServer, ServerHandler, ServiceExt, @@ -69,7 +71,7 @@ impl PatentHandler { pub async fn search_patents( &self, Parameters(request): Parameters, - ) -> String { + ) -> Result { let options = SearchOptions { query: request.query, assignee: request.assignee, @@ -81,10 +83,13 @@ impl PatentHandler { language: request.language, }; - match self.searcher.search(&options).await { - Ok(results) => serde_json::to_string_pretty(&results).unwrap_or_default(), - Err(e) => format!("Search failed: {}", e), - } + self.searcher + .search(&options) + .await + .map(|results| serde_json::to_string_pretty(&results).unwrap_or_default()) + .map_err(|e| { + ErrorData::new(ErrorCode::INTERNAL_ERROR, format!("Search failed: {}", e), None) + }) } /// Fetch details of a specific patent by ID @@ -92,13 +97,18 @@ impl PatentHandler { pub async fn fetch_patent( &self, Parameters(request): Parameters, - ) -> String { + ) -> Result { if request.raw { - match self.searcher.get_raw_html(&request.patent_id, request.language.as_deref()).await - { - Ok(html) => html, - Err(e) => format!("Failed to fetch raw HTML: {}", e), - } + self.searcher + .get_raw_html(&request.patent_id, request.language.as_deref()) + .await + .map_err(|e| { + ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Failed to fetch raw HTML: {}", e), + None, + ) + }) } else { let options = SearchOptions { query: None, @@ -112,10 +122,20 @@ impl PatentHandler { }; match self.searcher.search(&options).await { Ok(mut results) => results.patents.pop().map_or_else( - || format!("No patent found with ID: {}", request.patent_id), - |patent| serde_json::to_string_pretty(&patent).unwrap_or_default(), + || { + Err(ErrorData::new( + ErrorCode::INVALID_PARAMS, + format!("No patent found with ID: {}", request.patent_id), + None, + )) + }, + |patent| Ok(serde_json::to_string_pretty(&patent).unwrap_or_default()), ), - Err(e) => format!("Fetch failed: {}", e), + Err(e) => Err(ErrorData::new( + ErrorCode::INTERNAL_ERROR, + format!("Fetch failed: {}", e), + None, + )), } } } @@ -240,8 +260,10 @@ mod tests { language: None, }; let result = handler.search_patents(Parameters(request)).await; - assert!(result.contains("SEARCH1")); - assert!(result.contains("Search Result")); + assert!(result.is_ok()); + let result_str = result.unwrap(); + assert!(result_str.contains("SEARCH1")); + assert!(result_str.contains("Search Result")); } #[tokio::test] @@ -252,24 +274,30 @@ mod tests { let request = FetchPatentRequest { patent_id: "US123".to_string(), raw: false, language: None }; let result = handler.fetch_patent(Parameters(request)).await; - assert!(result.contains("US123")); + assert!(result.is_ok()); + assert!(result.unwrap().contains("US123")); // Raw HTML case let request = FetchPatentRequest { patent_id: "US123".to_string(), raw: true, language: None }; let result = handler.fetch_patent(Parameters(request)).await; - assert_eq!(result, "US123"); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "US123"); // Not found case let request = FetchPatentRequest { patent_id: "NONE".to_string(), raw: false, language: None }; let result = handler.fetch_patent(Parameters(request)).await; - assert!(result.contains("No patent found")); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.message.contains("No patent found")); // Error case let request = FetchPatentRequest { patent_id: "FAIL".to_string(), raw: false, language: None }; let result = handler.fetch_patent(Parameters(request)).await; - assert!(result.contains("Fetch failed")); + assert!(result.is_err()); + let err = result.unwrap_err(); + assert!(err.message.contains("Fetch failed")); } }