gsearch-mcp-cli 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ try:
4
+ __version__ = version("gsearch-mcp-cli")
5
+ except PackageNotFoundError:
6
+ __version__ = "0.0.1" # Keep in sync with pyproject.toml
File without changes
@@ -0,0 +1,83 @@
1
+ import asyncio
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+
7
+ from gsearch_mcp_cli import shared
8
+ from gsearch_mcp_cli.exceptions import GSearchError
9
+
10
+ console = Console()
11
+
12
+
13
+ TIME_CHOICES = click.Choice(["hour", "1h", "12h", "day", "24h", "week", "7d", "month", "year"], case_sensitive=False)
14
+
15
+
16
+ @click.command("ai")
17
+ @click.argument("query")
18
+ @click.option("--json", "json_output", is_flag=True, help="JSON output")
19
+ @click.option("--lang", default="en", help="Search language")
20
+ @click.option("--region", default=None, help="Search region (e.g., US, CA, GB)")
21
+ @click.option("--time", "time_filter", type=TIME_CHOICES, default=None,
22
+ help="Time filter: hour, day, week, month, year")
23
+ @click.option("--after", "date_from", default=None, help="Results after date (YYYY-MM-DD)")
24
+ @click.option("--before", "date_to", default=None, help="Results before date (YYYY-MM-DD)")
25
+ @click.option("--timeout", default=30, type=float, help="Timeout in seconds")
26
+ def ai_cmd(query, json_output, lang, region, time_filter, date_from, date_to, timeout):
27
+ """Run a Google AI Mode search and return synthesized results."""
28
+ async def _run():
29
+ try:
30
+ result = await shared.ai_search(
31
+ query=query,
32
+ language=lang,
33
+ region=region,
34
+ time_filter=time_filter,
35
+ date_from=date_from,
36
+ date_to=date_to,
37
+ timeout=timeout,
38
+ )
39
+
40
+ if json_output:
41
+ click.echo(result.model_dump_json(indent=2))
42
+ else:
43
+ _print_ai_result(result)
44
+
45
+ except GSearchError as e:
46
+ console.print(f"[red]Error:[/red] {e}")
47
+ raise SystemExit(1)
48
+ finally:
49
+ await shared.shutdown()
50
+
51
+ asyncio.run(_run())
52
+
53
+
54
+ def _print_ai_result(result):
55
+ """Pretty-print AI Mode results with Rich panels."""
56
+ if result.synthesis:
57
+ console.print()
58
+ console.print(Panel(
59
+ result.synthesis,
60
+ title=f"AI Synthesis — {result.query}",
61
+ border_style="green",
62
+ ))
63
+
64
+ if result.sections:
65
+ for section in result.sections:
66
+ body_parts = []
67
+ if section.content:
68
+ body_parts.append(section.content)
69
+ if section.items:
70
+ body_parts.extend(f" • {item}" for item in section.items)
71
+ console.print(Panel(
72
+ "\n".join(body_parts) if body_parts else "[dim]No content[/dim]",
73
+ title=section.heading,
74
+ border_style="cyan",
75
+ ))
76
+
77
+ if result.sources:
78
+ console.print("\n[bold]Sources[/bold]")
79
+ console.print("─" * 60)
80
+ for src in result.sources:
81
+ console.print(f" [link={src.url}]{src.domain}[/link] — {src.title}")
82
+
83
+ console.print()
@@ -0,0 +1,88 @@
1
+ AI_DOC = """gsearch-mcp-cli v{version} — Google Search CLI and MCP for AI Agents
2
+
3
+ SETUP
4
+ gsearch setup # One-time profile warmup (~8s)
5
+ gsearch setup --check # Verify profile works
6
+ gsearch setup --reset # Delete and recreate profile
7
+ gsearch setup add cursor # Configure MCP for Cursor
8
+ gsearch setup add claude-code # Configure MCP for Claude Code
9
+ gsearch setup list # Show all MCP setup targets
10
+ gsearch doctor # Diagnose browser, profile, connectivity
11
+
12
+ SEARCH (default command)
13
+ gsearch "query" # Regular Google search (default)
14
+ gsearch search "query" --json # JSON output
15
+ gsearch search "query" --no-ai # Skip AI Overview
16
+ gsearch search "query" -n 5 # Max 5 results
17
+ gsearch search "query" --lang fr # French results
18
+ gsearch search "query" --region CA # Canadian results
19
+
20
+ TIME-FILTERED SEARCH (use for news, recent events, time-sensitive queries)
21
+ gsearch "AI news" --time hour # Last hour
22
+ gsearch "AI news" --time 1h # Last hour (alias)
23
+ gsearch "news" --time 12h # Last 12 hours
24
+ gsearch "news" --time day # Last 24 hours
25
+ gsearch "news" --time 24h # Last 24 hours (alias)
26
+ gsearch "updates" --time week # Last 7 days
27
+ gsearch "updates" --time 7d # Last 7 days (alias)
28
+ gsearch "developments" --time month # Last month
29
+ gsearch "report" --time year # Last year
30
+ gsearch "events" --time 6h # Last 6 hours (arbitrary)
31
+ gsearch "earnings" --after 2026-01-01 --before 2026-03-31 # Custom date range
32
+
33
+ Time filter values: hour, 1h, 12h, day, 24h, week, 7d, month, 1m, year, 1y
34
+ Arbitrary hours: any "Nh" pattern (e.g., 3h, 6h, 18h)
35
+ Custom range: --after YYYY-MM-DD and/or --before YYYY-MM-DD
36
+
37
+ AI MODE (Google AI Mode / g.ai)
38
+ gsearch ai "query" # AI Mode synthesized response
39
+ gsearch ai "query" --json # JSON output
40
+ gsearch ai "query" --time day # With time filter
41
+ gsearch ai "query" --after 2026-01-01 # With date range
42
+
43
+ FETCH (URL to Markdown)
44
+ gsearch fetch "url" # Fetch and convert to Markdown
45
+ gsearch fetch "url" --raw # Plain text output
46
+ gsearch fetch "url" --json # JSON with metadata
47
+ gsearch fetch "url" --browser always # Force browser rendering
48
+
49
+ CONFIG
50
+ gsearch config show # Show all settings
51
+ gsearch config set browser brave # Set preferred browser
52
+ gsearch config set rate_limit 1.0 # Search throttle (seconds)
53
+
54
+ SKILL INSTALL
55
+ gsearch skill install cursor # Install for Cursor
56
+ gsearch skill install claude-code # Install for Claude Code
57
+ gsearch skill install cc-claw # Install for CC-Claw
58
+ gsearch skill list # Show all targets
59
+
60
+ MCP TOOLS (for AI agents via MCP protocol)
61
+ gsearch_search(query, max_results=10, include_ai=True, language="en",
62
+ time_filter=None, date_from=None, date_to=None, timeout_seconds=30)
63
+ gsearch_ai(query, language="en",
64
+ time_filter=None, date_from=None, date_to=None, timeout_seconds=30)
65
+ gsearch_fetch(url, format="md", use_browser="auto", timeout_seconds=30)
66
+
67
+ time_filter: "hour", "12h", "day", "week", "month", "year", or any "Nh" (e.g., "6h")
68
+ date_from / date_to: "YYYY-MM-DD" for custom date range
69
+
70
+ WHEN TO USE TIME FILTERS
71
+ "latest news about X" -> time_filter="day" or "12h"
72
+ "what happened today" -> time_filter="day"
73
+ "breaking news" -> time_filter="hour"
74
+ "recent developments" -> time_filter="week"
75
+ "this month's updates" -> time_filter="month"
76
+ "between Jan and March" -> date_from="2026-01-01", date_to="2026-03-31"
77
+
78
+ NOTES
79
+ - First run: gsearch setup runs automatically on first search
80
+ - No Google account needed: anonymous, pseudonymous sessions
81
+ - Rate limited: 1 search/2s default (configurable)
82
+ - Headless: no visible browser window
83
+ - SSRF protection: fetch validates URLs against private IP ranges
84
+ """
85
+
86
+
87
+ def get_ai_doc(version: str) -> str:
88
+ return AI_DOC.format(version=version)
@@ -0,0 +1,64 @@
1
+ """gsearch config — view and modify configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from gsearch_mcp_cli.config import DEFAULTS, Config
10
+ from gsearch_mcp_cli.exceptions import ConfigError
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.group("config")
16
+ def config_cmd() -> None:
17
+ """View and modify gsearch configuration."""
18
+
19
+
20
+ @config_cmd.command("show")
21
+ def config_show() -> None:
22
+ """Print all configuration values."""
23
+ cfg = Config()
24
+ data = cfg.show()
25
+
26
+ table = Table(title="gsearch configuration")
27
+ table.add_column("Key", style="cyan", no_wrap=True)
28
+ table.add_column("Value", style="green")
29
+ table.add_column("Default", style="dim")
30
+
31
+ for key in sorted(data):
32
+ current = data[key]
33
+ default = DEFAULTS.get(key)
34
+ style = "" if current == default else "bold yellow"
35
+ table.add_row(key, str(current), str(default), style=style)
36
+
37
+ console.print(table)
38
+
39
+
40
+ @config_cmd.command("get")
41
+ @click.argument("key")
42
+ def config_get(key: str) -> None:
43
+ """Get a single configuration value."""
44
+ cfg = Config()
45
+ try:
46
+ value = cfg.get(key)
47
+ console.print(f"[cyan]{key}[/] = [green]{value}[/]")
48
+ except ConfigError as exc:
49
+ console.print(f"[bold red]Error:[/] {exc}")
50
+ raise SystemExit(1)
51
+
52
+
53
+ @config_cmd.command("set")
54
+ @click.argument("key")
55
+ @click.argument("value")
56
+ def config_set(key: str, value: str) -> None:
57
+ """Set a configuration value."""
58
+ cfg = Config()
59
+ try:
60
+ cfg.set(key, value)
61
+ console.print(f"[green]✓[/] Set [cyan]{key}[/] = [green]{cfg.get(key)}[/]")
62
+ except ConfigError as exc:
63
+ console.print(f"[bold red]Error:[/] {exc}")
64
+ raise SystemExit(1)
@@ -0,0 +1,77 @@
1
+ """gsearch doctor — run diagnostic checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import click
6
+ from rich.console import Console
7
+
8
+ from gsearch_mcp_cli.config import Config
9
+ from gsearch_mcp_cli.constants import CHROME_PROFILE_DIR
10
+ from gsearch_mcp_cli.core.browser import find_browser
11
+ from gsearch_mcp_cli.exceptions import BrowserNotFoundError
12
+
13
+ console = Console()
14
+
15
+ PASS = "[bold green]✓[/]"
16
+ FAIL = "[bold red]✗[/]"
17
+
18
+
19
+ def _check_browser() -> bool:
20
+ try:
21
+ path = find_browser()
22
+ console.print(f" {PASS} Browser found: {path}")
23
+ return True
24
+ except BrowserNotFoundError as exc:
25
+ console.print(f" {FAIL} Browser not found: {exc}")
26
+ return False
27
+
28
+
29
+ def _check_profile_dir() -> bool:
30
+ if CHROME_PROFILE_DIR.exists():
31
+ console.print(f" {PASS} Profile directory exists: {CHROME_PROFILE_DIR}")
32
+ return True
33
+ console.print(f" {FAIL} Profile directory missing: {CHROME_PROFILE_DIR}")
34
+ return False
35
+
36
+
37
+ def _check_warmed_up() -> bool:
38
+ marker = CHROME_PROFILE_DIR / ".warmed_up"
39
+ if marker.exists():
40
+ console.print(f" {PASS} Profile is warmed up")
41
+ return True
42
+ console.print(f" {FAIL} Profile not warmed up — run: gsearch setup")
43
+ return False
44
+
45
+
46
+ def _check_config() -> bool:
47
+ try:
48
+ cfg = Config()
49
+ cfg.show()
50
+ console.print(f" {PASS} Config file valid")
51
+ return True
52
+ except Exception as exc:
53
+ console.print(f" {FAIL} Config error: {exc}")
54
+ return False
55
+
56
+
57
+ @click.command("doctor")
58
+ def doctor_cmd() -> None:
59
+ """Run diagnostic checks on gsearch environment."""
60
+ console.print("[bold]Running diagnostics…[/]\n")
61
+
62
+ checks = [
63
+ ("Browser", _check_browser),
64
+ ("Profile directory", _check_profile_dir),
65
+ ("Profile warmup", _check_warmed_up),
66
+ ("Configuration", _check_config),
67
+ ]
68
+
69
+ passed = sum(1 for _, fn in checks if fn())
70
+ total = len(checks)
71
+
72
+ console.print()
73
+ if passed == total:
74
+ console.print(f"[bold green]All {total} checks passed.[/]")
75
+ else:
76
+ console.print(f"[bold yellow]{passed}/{total} checks passed.[/]")
77
+ raise SystemExit(1)
@@ -0,0 +1,50 @@
1
+ import asyncio
2
+
3
+ import click
4
+ from rich.console import Console
5
+
6
+ from gsearch_mcp_cli import shared
7
+ from gsearch_mcp_cli.exceptions import GSearchError
8
+
9
+ console = Console()
10
+
11
+
12
+ @click.command("fetch")
13
+ @click.argument("url")
14
+ @click.option("--raw", is_flag=True, help="Output plain text instead of markdown")
15
+ @click.option("--json", "json_output", is_flag=True, help="JSON output")
16
+ @click.option("--browser", type=click.Choice(["auto", "always", "never"]),
17
+ default="auto", help="Browser usage mode")
18
+ @click.option("--timeout", default=30, type=float, help="Timeout in seconds")
19
+ def fetch_cmd(url, raw, json_output, browser, timeout):
20
+ """Fetch a URL and return its content."""
21
+ async def _run():
22
+ try:
23
+ fmt = "txt" if raw else "md"
24
+ result = await shared.fetch(
25
+ url=url,
26
+ format=fmt,
27
+ use_browser=browser,
28
+ timeout=timeout,
29
+ )
30
+
31
+ if json_output:
32
+ click.echo(result.model_dump_json(indent=2))
33
+ else:
34
+ _print_fetch_result(result, raw=raw)
35
+
36
+ except GSearchError as e:
37
+ console.print(f"[red]Error:[/red] {e}")
38
+ raise SystemExit(1)
39
+ finally:
40
+ await shared.shutdown()
41
+
42
+ asyncio.run(_run())
43
+
44
+
45
+ def _print_fetch_result(result, *, raw=False):
46
+ """Print fetched content."""
47
+ if not raw:
48
+ console.print(f"\n[bold]{result.title}[/bold]")
49
+ console.print(f"[dim]{result.url} • {result.word_count} words[/dim]\n")
50
+ click.echo(result.content)
@@ -0,0 +1,64 @@
1
+ import click
2
+
3
+ from gsearch_mcp_cli import __version__
4
+ from gsearch_mcp_cli.cli.ai_cmd import ai_cmd
5
+ from gsearch_mcp_cli.cli.ai_doc import get_ai_doc
6
+ from gsearch_mcp_cli.cli.config_cmd import config_cmd
7
+ from gsearch_mcp_cli.cli.doctor_cmd import doctor_cmd
8
+ from gsearch_mcp_cli.cli.fetch_cmd import fetch_cmd
9
+ from gsearch_mcp_cli.cli.search_cmd import search_cmd
10
+ from gsearch_mcp_cli.cli.setup_cmd import setup_cmd
11
+ from gsearch_mcp_cli.cli.skill_cmd import skill_cmd
12
+
13
+
14
+ def _print_docs(ctx, param, value):
15
+ if not value or ctx.resilient_parsing:
16
+ return
17
+ click.echo(get_ai_doc(__version__))
18
+ ctx.exit()
19
+
20
+
21
+ class DefaultSearchGroup(click.Group):
22
+ """Click group that falls back to 'search' for unknown commands."""
23
+
24
+ def parse_args(self, ctx, args):
25
+ if args and not args[0].startswith("-"):
26
+ if args[0] not in self.commands:
27
+ args = ["search"] + args
28
+ return super().parse_args(ctx, args)
29
+
30
+
31
+ @click.group(cls=DefaultSearchGroup, invoke_without_command=True)
32
+ @click.version_option(__version__, prog_name="gsearch")
33
+ @click.option("--ai", is_flag=True, callback=_print_docs, expose_value=False,
34
+ is_eager=True, help="Print AI-optimized documentation")
35
+ @click.option("--verbose", is_flag=True, default=False, help="Enable debug logging")
36
+ @click.pass_context
37
+ def cli(ctx, verbose):
38
+ """gsearch — Google Search CLI for AI agents."""
39
+ ctx.ensure_object(dict)
40
+ ctx.obj["verbose"] = verbose
41
+
42
+ if verbose:
43
+ import logging
44
+ logging.basicConfig(level=logging.DEBUG)
45
+
46
+ if ctx.invoked_subcommand is None:
47
+ click.echo(ctx.get_help())
48
+
49
+
50
+ cli.add_command(search_cmd, "search")
51
+ cli.add_command(ai_cmd, "ai")
52
+ cli.add_command(fetch_cmd, "fetch")
53
+ cli.add_command(skill_cmd, "skill")
54
+ cli.add_command(setup_cmd, "setup")
55
+ cli.add_command(config_cmd, "config")
56
+ cli.add_command(doctor_cmd, "doctor")
57
+
58
+
59
+ def main():
60
+ cli()
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
@@ -0,0 +1,92 @@
1
+ import asyncio
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+
7
+ from gsearch_mcp_cli import shared
8
+ from gsearch_mcp_cli.exceptions import GSearchError
9
+
10
+ console = Console()
11
+
12
+
13
+ TIME_CHOICES = click.Choice(["hour", "1h", "12h", "day", "24h", "week", "7d", "month", "year"], case_sensitive=False)
14
+
15
+
16
+ @click.command("search")
17
+ @click.argument("query")
18
+ @click.option("--max-results", "-n", default=10, help="Max organic results")
19
+ @click.option("--json", "json_output", is_flag=True, help="JSON output")
20
+ @click.option("--no-ai", is_flag=True, help="Skip AI Overview extraction")
21
+ @click.option("--lang", default="en", help="Search language")
22
+ @click.option("--region", default=None, help="Search region (e.g., US, CA, GB)")
23
+ @click.option("--time", "time_filter", type=TIME_CHOICES, default=None,
24
+ help="Time filter: hour, day, week, month, year")
25
+ @click.option("--after", "date_from", default=None, help="Results after date (YYYY-MM-DD)")
26
+ @click.option("--before", "date_to", default=None, help="Results before date (YYYY-MM-DD)")
27
+ @click.option("--timeout", default=30, type=float, help="Timeout in seconds")
28
+ def search_cmd(query, max_results, json_output, no_ai, lang, region, time_filter, date_from, date_to, timeout):
29
+ """Search Google and return results with AI Overview."""
30
+ async def _run():
31
+ try:
32
+ result = await shared.search(
33
+ query=query,
34
+ max_results=max_results,
35
+ include_ai=not no_ai,
36
+ language=lang,
37
+ region=region,
38
+ time_filter=time_filter,
39
+ date_from=date_from,
40
+ date_to=date_to,
41
+ timeout=timeout,
42
+ )
43
+
44
+ if json_output:
45
+ click.echo(result.model_dump_json(indent=2))
46
+ else:
47
+ _print_results(result)
48
+
49
+ except GSearchError as e:
50
+ console.print(f"[red]Error:[/red] {e}")
51
+ raise SystemExit(1)
52
+ finally:
53
+ await shared.shutdown()
54
+
55
+ asyncio.run(_run())
56
+
57
+
58
+ def _print_results(result):
59
+ """Pretty-print search results with Rich."""
60
+ if result.ai_overview:
61
+ console.print()
62
+ console.print(Panel(
63
+ result.ai_overview.text[:1000],
64
+ title="AI Overview",
65
+ border_style="blue",
66
+ ))
67
+ if result.ai_overview.sources:
68
+ sources = ", ".join(s.domain for s in result.ai_overview.sources)
69
+ console.print(f" [dim]Sources: {sources}[/dim]")
70
+ console.print()
71
+
72
+ if result.results:
73
+ console.print("[bold]Search Results[/bold]")
74
+ console.print("─" * 60)
75
+ for r in result.results:
76
+ console.print(f"\n[bold]{r.position}. {r.title}[/bold]")
77
+ console.print(f" [link={r.url}]{r.url}[/link]")
78
+ if r.snippet:
79
+ console.print(f" [dim]{r.snippet[:200]}[/dim]")
80
+
81
+ if result.people_also_ask:
82
+ console.print("\n[bold]People Also Ask[/bold]")
83
+ console.print("─" * 60)
84
+ for q in result.people_also_ask:
85
+ console.print(f" • {q}")
86
+
87
+ if result.related_searches:
88
+ console.print("\n[bold]Related Searches[/bold]")
89
+ for q in result.related_searches:
90
+ console.print(f" • {q}")
91
+
92
+ console.print()