searchfetch 2.0.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Max
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: searchfetch
3
+ Version: 2.0.0
4
+ Summary: A maximum fault-tolerant, stealth-enabled MCP server for web searching and fetching.
5
+ Author: Max
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/maxylev/searchfetch
8
+ Project-URL: Repository, https://github.com/maxylev/searchfetch
9
+ Keywords: mcp,search,fetch,llm,ai,agent,stealth
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: mcp>=1.27.1
14
+ Requires-Dist: cloakbrowser>=0.3.26
15
+ Requires-Dist: beautifulsoup4>=4.12.0
16
+ Requires-Dist: markdownify>=0.11.0
17
+ Dynamic: license-file
18
+
19
+ # SearchFetch (MCP Server)
20
+
21
+ A maximum fault-tolerant, stealth-enabled Model Context Protocol (MCP) server for web searching and content fetching. Built specifically for AI Agents (Cursor, Claude Desktop, OpenHands), it completely bypasses bot detection (Cloudflare Turnstile, Datadome), dynamically handles SPAs/React, and converts bloat into token-optimized Markdown.
22
+
23
+ ## Features
24
+ * **Maximum Fault Tolerance:** Implements auto-healing browser sessions, grace-period timeouts for clunky SPAs, and network-level aborting of tracking scripts and media.
25
+ * **Stealth Engine:** Powered by CloakBrowser C++ patches + `humanize` logic. Antibot systems score it as a normal browser because it mathematically moves and renders exactly like one.
26
+ * **Nuclear Token Scrubber:** Strips Base64 images, SVGs, scripts, and inline styles out of the DOM *before* Markdown conversion, guaranteeing your LLM context window won't blow out.
27
+ * **Dual Execution Paths:** Natively supports zero-install execution via both Python (`uvx`) and Node.js (`npx`).
28
+
29
+ ---
30
+
31
+ ## Usage & Installation
32
+
33
+ You do not need to install this repository manually. Configure your agent to use the zero-install commands `npx` or `uvx` depending on your environment.
34
+
35
+ ### Claude Desktop Configuration
36
+ Add the following to your config:
37
+
38
+ **Option A: Using Python (`uvx` - Recommended)**
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "searchfetch": {
43
+ "command": "uvx",
44
+ "args": ["searchfetch"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ **Option B: Using Node.js (`npx`)**
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "searchfetch": {
55
+ "command": "npx",
56
+ "args": ["-y", "searchfetch"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Cursor / IDE Configuration
63
+ Add it via the **MCP panel** in Cursor settings:
64
+ * **Type:** `command`
65
+ * **Command:** `uvx searchfetch` (or `npx -y searchfetch`)
66
+
67
+ ---
68
+
69
+ ## Available Tools
70
+
71
+ ### 1. `websearch`
72
+ Searches the web using DuckDuckGo or Google. Returns a clean list of titles, URLs, and snippets. Excellent for researching general knowledge, news, and finding URLs.
73
+
74
+ **Parameters:**
75
+ * **`query`** *(string, required)*: The search query string.
76
+ * **`engine`** *(string, optional)*: Search engine to use. Can be `"duckduckgo"` or `"google"`. Default is `"duckduckgo"`.
77
+ * **`max_results`** *(number, optional)*: Maximum number of results to return. Default is `10`.
78
+ * **`region`** *(string, optional)*: Region and language code to localize search results.
79
+ * Examples: `"us-en"`, `"uk-en"`, `"de-de"`.
80
+ * For DuckDuckGo, it maps directly.
81
+ * For Google, it maps to the `gl` (country) and `hl` (language) query parameters automatically.
82
+ * Default is `"wt-wt"` (global/US English).
83
+ * **`safe_search`** *(string, optional)*: Safe search filtering mode.
84
+ * `"-1"` for Moderate.
85
+ * `"1"` for Strict.
86
+ * `"-2"` for Off.
87
+ * Default is `"-1"`.
88
+ * *Note: Only applies to DuckDuckGo.*
89
+
90
+ ### 2. `webfetch`
91
+ Fetch and extract the main text content from any webpage. Fully executes JavaScript to load React/SPAs and aggressively strips images/media (including base64) to save context tokens.
92
+
93
+ **Parameters:**
94
+ * **`url`** *(string, required)*: The full URL of the webpage to fetch (must start with http/https).
95
+ * **`format`** *(string, optional)*: Output format. Set to `"markdown"`, `"clean_html"`, or `"raw_html"`. Default is `"markdown"` (highly recommended to save context tokens).
96
+ * **`start_index`** *(number, optional)*: Character offset to start reading from for pagination. Use this if a document is too large to fit in the context window. Default is `0`.
97
+ * **`max_length`** *(number, optional)*: Maximum characters to return per request. Default is `10000`.
98
+ * **`block_media`** *(boolean, optional)*: Block images, videos, and fonts entirely at the network layer to drastically speed up page loads and dodge tracking pixels. Default is `true`.
99
+
100
+ ---
101
+
102
+ ## Architecture & Contributions
103
+ This repository utilizes a flat dual-manifest file structure (`package.json` and `pyproject.toml` in the root). When committing changes, ensure parity between `index.js` and `server.py` logic.
104
+
105
+ ### Local Development
106
+ ```bash
107
+ # Node.js Testing
108
+ npm i
109
+ npm run inspector-js
110
+
111
+ # Python Testing
112
+ pip install -e .
113
+ npm run inspector-py
114
+ ```
@@ -0,0 +1,96 @@
1
+ # SearchFetch (MCP Server)
2
+
3
+ A maximum fault-tolerant, stealth-enabled Model Context Protocol (MCP) server for web searching and content fetching. Built specifically for AI Agents (Cursor, Claude Desktop, OpenHands), it completely bypasses bot detection (Cloudflare Turnstile, Datadome), dynamically handles SPAs/React, and converts bloat into token-optimized Markdown.
4
+
5
+ ## Features
6
+ * **Maximum Fault Tolerance:** Implements auto-healing browser sessions, grace-period timeouts for clunky SPAs, and network-level aborting of tracking scripts and media.
7
+ * **Stealth Engine:** Powered by CloakBrowser C++ patches + `humanize` logic. Antibot systems score it as a normal browser because it mathematically moves and renders exactly like one.
8
+ * **Nuclear Token Scrubber:** Strips Base64 images, SVGs, scripts, and inline styles out of the DOM *before* Markdown conversion, guaranteeing your LLM context window won't blow out.
9
+ * **Dual Execution Paths:** Natively supports zero-install execution via both Python (`uvx`) and Node.js (`npx`).
10
+
11
+ ---
12
+
13
+ ## Usage & Installation
14
+
15
+ You do not need to install this repository manually. Configure your agent to use the zero-install commands `npx` or `uvx` depending on your environment.
16
+
17
+ ### Claude Desktop Configuration
18
+ Add the following to your config:
19
+
20
+ **Option A: Using Python (`uvx` - Recommended)**
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "searchfetch": {
25
+ "command": "uvx",
26
+ "args": ["searchfetch"]
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ **Option B: Using Node.js (`npx`)**
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "searchfetch": {
37
+ "command": "npx",
38
+ "args": ["-y", "searchfetch"]
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Cursor / IDE Configuration
45
+ Add it via the **MCP panel** in Cursor settings:
46
+ * **Type:** `command`
47
+ * **Command:** `uvx searchfetch` (or `npx -y searchfetch`)
48
+
49
+ ---
50
+
51
+ ## Available Tools
52
+
53
+ ### 1. `websearch`
54
+ Searches the web using DuckDuckGo or Google. Returns a clean list of titles, URLs, and snippets. Excellent for researching general knowledge, news, and finding URLs.
55
+
56
+ **Parameters:**
57
+ * **`query`** *(string, required)*: The search query string.
58
+ * **`engine`** *(string, optional)*: Search engine to use. Can be `"duckduckgo"` or `"google"`. Default is `"duckduckgo"`.
59
+ * **`max_results`** *(number, optional)*: Maximum number of results to return. Default is `10`.
60
+ * **`region`** *(string, optional)*: Region and language code to localize search results.
61
+ * Examples: `"us-en"`, `"uk-en"`, `"de-de"`.
62
+ * For DuckDuckGo, it maps directly.
63
+ * For Google, it maps to the `gl` (country) and `hl` (language) query parameters automatically.
64
+ * Default is `"wt-wt"` (global/US English).
65
+ * **`safe_search`** *(string, optional)*: Safe search filtering mode.
66
+ * `"-1"` for Moderate.
67
+ * `"1"` for Strict.
68
+ * `"-2"` for Off.
69
+ * Default is `"-1"`.
70
+ * *Note: Only applies to DuckDuckGo.*
71
+
72
+ ### 2. `webfetch`
73
+ Fetch and extract the main text content from any webpage. Fully executes JavaScript to load React/SPAs and aggressively strips images/media (including base64) to save context tokens.
74
+
75
+ **Parameters:**
76
+ * **`url`** *(string, required)*: The full URL of the webpage to fetch (must start with http/https).
77
+ * **`format`** *(string, optional)*: Output format. Set to `"markdown"`, `"clean_html"`, or `"raw_html"`. Default is `"markdown"` (highly recommended to save context tokens).
78
+ * **`start_index`** *(number, optional)*: Character offset to start reading from for pagination. Use this if a document is too large to fit in the context window. Default is `0`.
79
+ * **`max_length`** *(number, optional)*: Maximum characters to return per request. Default is `10000`.
80
+ * **`block_media`** *(boolean, optional)*: Block images, videos, and fonts entirely at the network layer to drastically speed up page loads and dodge tracking pixels. Default is `true`.
81
+
82
+ ---
83
+
84
+ ## Architecture & Contributions
85
+ This repository utilizes a flat dual-manifest file structure (`package.json` and `pyproject.toml` in the root). When committing changes, ensure parity between `index.js` and `server.py` logic.
86
+
87
+ ### Local Development
88
+ ```bash
89
+ # Node.js Testing
90
+ npm i
91
+ npm run inspector-js
92
+
93
+ # Python Testing
94
+ pip install -e .
95
+ npm run inspector-py
96
+ ```
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "searchfetch"
7
+ version = "2.0.0"
8
+ description = "A maximum fault-tolerant, stealth-enabled MCP server for web searching and fetching."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.10"
12
+ authors =[
13
+ { name = "Max" }
14
+ ]
15
+ keywords =["mcp", "search", "fetch", "llm", "ai", "agent", "stealth"]
16
+ dependencies =[
17
+ "mcp>=1.27.1",
18
+ "cloakbrowser>=0.3.26",
19
+ "beautifulsoup4>=4.12.0",
20
+ "markdownify>=0.11.0"
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/maxylev/searchfetch"
25
+ Repository = "https://github.com/maxylev/searchfetch"
26
+
27
+ [project.scripts]
28
+ mcp-searchfetch = "server:main"
29
+ searchfetch = "server:main"
30
+
31
+ [tool.setuptools]
32
+ py-modules =["server"]
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: searchfetch
3
+ Version: 2.0.0
4
+ Summary: A maximum fault-tolerant, stealth-enabled MCP server for web searching and fetching.
5
+ Author: Max
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/maxylev/searchfetch
8
+ Project-URL: Repository, https://github.com/maxylev/searchfetch
9
+ Keywords: mcp,search,fetch,llm,ai,agent,stealth
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: mcp>=1.27.1
14
+ Requires-Dist: cloakbrowser>=0.3.26
15
+ Requires-Dist: beautifulsoup4>=4.12.0
16
+ Requires-Dist: markdownify>=0.11.0
17
+ Dynamic: license-file
18
+
19
+ # SearchFetch (MCP Server)
20
+
21
+ A maximum fault-tolerant, stealth-enabled Model Context Protocol (MCP) server for web searching and content fetching. Built specifically for AI Agents (Cursor, Claude Desktop, OpenHands), it completely bypasses bot detection (Cloudflare Turnstile, Datadome), dynamically handles SPAs/React, and converts bloat into token-optimized Markdown.
22
+
23
+ ## Features
24
+ * **Maximum Fault Tolerance:** Implements auto-healing browser sessions, grace-period timeouts for clunky SPAs, and network-level aborting of tracking scripts and media.
25
+ * **Stealth Engine:** Powered by CloakBrowser C++ patches + `humanize` logic. Antibot systems score it as a normal browser because it mathematically moves and renders exactly like one.
26
+ * **Nuclear Token Scrubber:** Strips Base64 images, SVGs, scripts, and inline styles out of the DOM *before* Markdown conversion, guaranteeing your LLM context window won't blow out.
27
+ * **Dual Execution Paths:** Natively supports zero-install execution via both Python (`uvx`) and Node.js (`npx`).
28
+
29
+ ---
30
+
31
+ ## Usage & Installation
32
+
33
+ You do not need to install this repository manually. Configure your agent to use the zero-install commands `npx` or `uvx` depending on your environment.
34
+
35
+ ### Claude Desktop Configuration
36
+ Add the following to your config:
37
+
38
+ **Option A: Using Python (`uvx` - Recommended)**
39
+ ```json
40
+ {
41
+ "mcpServers": {
42
+ "searchfetch": {
43
+ "command": "uvx",
44
+ "args": ["searchfetch"]
45
+ }
46
+ }
47
+ }
48
+ ```
49
+
50
+ **Option B: Using Node.js (`npx`)**
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "searchfetch": {
55
+ "command": "npx",
56
+ "args": ["-y", "searchfetch"]
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Cursor / IDE Configuration
63
+ Add it via the **MCP panel** in Cursor settings:
64
+ * **Type:** `command`
65
+ * **Command:** `uvx searchfetch` (or `npx -y searchfetch`)
66
+
67
+ ---
68
+
69
+ ## Available Tools
70
+
71
+ ### 1. `websearch`
72
+ Searches the web using DuckDuckGo or Google. Returns a clean list of titles, URLs, and snippets. Excellent for researching general knowledge, news, and finding URLs.
73
+
74
+ **Parameters:**
75
+ * **`query`** *(string, required)*: The search query string.
76
+ * **`engine`** *(string, optional)*: Search engine to use. Can be `"duckduckgo"` or `"google"`. Default is `"duckduckgo"`.
77
+ * **`max_results`** *(number, optional)*: Maximum number of results to return. Default is `10`.
78
+ * **`region`** *(string, optional)*: Region and language code to localize search results.
79
+ * Examples: `"us-en"`, `"uk-en"`, `"de-de"`.
80
+ * For DuckDuckGo, it maps directly.
81
+ * For Google, it maps to the `gl` (country) and `hl` (language) query parameters automatically.
82
+ * Default is `"wt-wt"` (global/US English).
83
+ * **`safe_search`** *(string, optional)*: Safe search filtering mode.
84
+ * `"-1"` for Moderate.
85
+ * `"1"` for Strict.
86
+ * `"-2"` for Off.
87
+ * Default is `"-1"`.
88
+ * *Note: Only applies to DuckDuckGo.*
89
+
90
+ ### 2. `webfetch`
91
+ Fetch and extract the main text content from any webpage. Fully executes JavaScript to load React/SPAs and aggressively strips images/media (including base64) to save context tokens.
92
+
93
+ **Parameters:**
94
+ * **`url`** *(string, required)*: The full URL of the webpage to fetch (must start with http/https).
95
+ * **`format`** *(string, optional)*: Output format. Set to `"markdown"`, `"clean_html"`, or `"raw_html"`. Default is `"markdown"` (highly recommended to save context tokens).
96
+ * **`start_index`** *(number, optional)*: Character offset to start reading from for pagination. Use this if a document is too large to fit in the context window. Default is `0`.
97
+ * **`max_length`** *(number, optional)*: Maximum characters to return per request. Default is `10000`.
98
+ * **`block_media`** *(boolean, optional)*: Block images, videos, and fonts entirely at the network layer to drastically speed up page loads and dodge tracking pixels. Default is `true`.
99
+
100
+ ---
101
+
102
+ ## Architecture & Contributions
103
+ This repository utilizes a flat dual-manifest file structure (`package.json` and `pyproject.toml` in the root). When committing changes, ensure parity between `index.js` and `server.py` logic.
104
+
105
+ ### Local Development
106
+ ```bash
107
+ # Node.js Testing
108
+ npm i
109
+ npm run inspector-js
110
+
111
+ # Python Testing
112
+ pip install -e .
113
+ npm run inspector-py
114
+ ```
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ server.py
5
+ searchfetch.egg-info/PKG-INFO
6
+ searchfetch.egg-info/SOURCES.txt
7
+ searchfetch.egg-info/dependency_links.txt
8
+ searchfetch.egg-info/entry_points.txt
9
+ searchfetch.egg-info/requires.txt
10
+ searchfetch.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ mcp-searchfetch = server:main
3
+ searchfetch = server:main
@@ -0,0 +1,4 @@
1
+ mcp>=1.27.1
2
+ cloakbrowser>=0.3.26
3
+ beautifulsoup4>=4.12.0
4
+ markdownify>=0.11.0
@@ -0,0 +1 @@
1
+ server
@@ -0,0 +1,321 @@
1
+ import asyncio
2
+ import re
3
+ import urllib.parse
4
+
5
+ from bs4 import BeautifulSoup, Tag
6
+ from cloakbrowser import launch_async
7
+ from markdownify import markdownify as md
8
+ from mcp.server.fastmcp import FastMCP
9
+ from playwright.async_api import TimeoutError as PlaywrightTimeoutError
10
+
11
+ mcp = FastMCP("searchfetch")
12
+
13
+
14
+ class BrowserManager:
15
+ def __init__(self):
16
+ self.browser = None
17
+ self._lock = asyncio.Lock()
18
+
19
+ async def get_browser(self):
20
+ async with self._lock:
21
+ if self.browser and self.browser.is_connected:
22
+ return self.browser
23
+
24
+ self.browser = await launch_async(
25
+ headless=True,
26
+ humanize=True,
27
+ args=[
28
+ "--disable-blink-features=AutomationControlled",
29
+ "--no-sandbox",
30
+ "--disable-dev-shm-usage",
31
+ ],
32
+ )
33
+ return self.browser
34
+
35
+ async def close(self):
36
+ if self.browser:
37
+ await self.browser.close()
38
+ self.browser = None
39
+
40
+
41
+ browser_manager = BrowserManager()
42
+
43
+
44
+ def get_google_region_params(region: str) -> str:
45
+ if not region or region == "wt-wt":
46
+ return "hl=en&gl=us"
47
+ parts = region.split("-")
48
+ if len(parts) == 2:
49
+ return f"gl={parts[0]}&hl={parts[1]}"
50
+ return f"gl={region}&hl=en"
51
+
52
+
53
+ def _get_string_attr(tag: Tag, attr: str) -> str:
54
+ val = tag.get(attr)
55
+ if isinstance(val, list):
56
+ return str(val[0]) if val else ""
57
+ return str(val or "")
58
+
59
+
60
+ @mcp.tool()
61
+ async def websearch(
62
+ query: str,
63
+ engine: str = "duckduckgo",
64
+ max_results: int = 10,
65
+ region: str = "wt-wt",
66
+ safe_search: str = "-1",
67
+ ) -> str:
68
+ """
69
+ Search the web using DuckDuckGo or Google. Returns a clean list of titles, URLs, and snippets. Excellent for researching general knowledge, news, and finding URLs.
70
+
71
+ Args:
72
+ query: The search query string.
73
+ engine: Search engine to use. Can be "duckduckgo" or "google". Default is "duckduckgo".
74
+ max_results: Maximum number of results to return. Default is 10.
75
+ region: Region and language code to localize search results (e.g., "us-en", "uk-en", "de-de"). For DuckDuckGo it maps directly. For Google, 'us' is country code and 'en' is language. Default is "wt-wt" (global/US English).
76
+ safe_search: Safe search filtering mode. "-1" for Moderate, "1" for Strict, "-2" for Off. Default is "-1". Note: Only applies to DuckDuckGo.
77
+ """
78
+ try:
79
+ browser = await browser_manager.get_browser()
80
+ context = await browser.new_context()
81
+
82
+ await context.add_cookies(
83
+ [
84
+ {
85
+ "name": "CONSENT",
86
+ "value": "YES+cb.20250101-01-p0.en+FX+999",
87
+ "domain": ".google.com",
88
+ "path": "/",
89
+ }
90
+ ]
91
+ )
92
+
93
+ page = await context.new_page()
94
+
95
+ async def route_handler(route):
96
+ if route.request.resource_type in ["image", "media", "font", "stylesheet"]:
97
+ await route.abort()
98
+ else:
99
+ await route.continue_()
100
+
101
+ await page.route("**/*", route_handler)
102
+
103
+ if engine.lower() == "google":
104
+ search_url = f"https://www.google.com/search?udm=web&udm=14&q={urllib.parse.quote(query)}&{get_google_region_params(region)}"
105
+ else:
106
+ search_url = f"https://html.duckduckgo.com/html/?q={urllib.parse.quote(query)}&kl={urllib.parse.quote(region)}&kp={urllib.parse.quote(safe_search)}"
107
+
108
+ try:
109
+ await page.goto(search_url, wait_until="networkidle", timeout=15000)
110
+ except PlaywrightTimeoutError:
111
+ pass
112
+
113
+ content = await page.content()
114
+ soup = BeautifulSoup(content, "html.parser")
115
+ results = []
116
+
117
+ if engine.lower() == "google":
118
+ for h3 in soup.find_all("h3"):
119
+ if len(results) >= max_results:
120
+ break
121
+
122
+ if not isinstance(h3, Tag):
123
+ continue
124
+
125
+ link_el = h3.find_parent("a")
126
+ if not link_el:
127
+ link_el = h3.find("a")
128
+
129
+ if not isinstance(link_el, Tag):
130
+ continue
131
+
132
+ link = _get_string_attr(link_el, "href")
133
+
134
+ if not link or (
135
+ link.startswith("/") and not link.startswith("/url?q=")
136
+ ):
137
+ continue
138
+ if "google.com/search" in link or "support.google.com" in link:
139
+ continue
140
+
141
+ if link.startswith("/url?q="):
142
+ try:
143
+ link = urllib.parse.unquote(
144
+ link.split("/url?q=")[1].split("&")[0]
145
+ )
146
+ except Exception:
147
+ pass
148
+
149
+ title = h3.get_text(strip=True)
150
+ if not title:
151
+ continue
152
+
153
+ snippet = ""
154
+ parent = h3.parent
155
+ while parent and getattr(parent, "name", None) != "body":
156
+ if isinstance(parent, Tag):
157
+ snippet_el = parent.select_one(
158
+ "div.VwiC3b, div[style*='-webkit-line-clamp'], div.yXK7lf, div.Uroaid"
159
+ )
160
+ if snippet_el:
161
+ snippet = re.sub(
162
+ r"\s+", " ", snippet_el.get_text(strip=True)
163
+ )
164
+ break
165
+ parent = parent.parent
166
+
167
+ if link.startswith("http"):
168
+ if not any(link in r for r in results):
169
+ results.append(
170
+ f"[{len(results) + 1}] {title}\n URL: {link}\n Summary: {snippet}"
171
+ )
172
+ else:
173
+ for el in soup.select(".result"):
174
+ if len(results) >= max_results:
175
+ break
176
+
177
+ if not isinstance(el, Tag):
178
+ continue
179
+
180
+ title_el = el.select_one(".result__title a")
181
+ if not title_el:
182
+ continue
183
+
184
+ link = _get_string_attr(title_el, "href")
185
+ if "/l/?uddg=" in link:
186
+ try:
187
+ params = dict(
188
+ urllib.parse.parse_qsl(urllib.parse.urlsplit(link).query)
189
+ )
190
+ link = urllib.parse.unquote(str(params.get("uddg", link)))
191
+ except Exception:
192
+ pass
193
+
194
+ snippet_el = el.select_one(".result__snippet")
195
+ snippet = (
196
+ re.sub(r"\s+", " ", snippet_el.get_text()).strip()
197
+ if snippet_el
198
+ else ""
199
+ )
200
+
201
+ if link.startswith("http"):
202
+ results.append(
203
+ f"[{len(results) + 1}] {title_el.get_text(strip=True)}\n URL: {link}\n Summary: {snippet}"
204
+ )
205
+
206
+ await context.close()
207
+
208
+ if not results:
209
+ return f"No results found on {engine}. The engine may have shown a captcha, or the query returned nothing."
210
+
211
+ return f"Found {len(results)} search results on {engine}:\n\n" + "\n\n".join(
212
+ results
213
+ )
214
+
215
+ except Exception as e:
216
+ return f"An error occurred while executing the search: {str(e)}"
217
+
218
+
219
+ @mcp.tool()
220
+ async def webfetch(
221
+ url: str,
222
+ format: str = "markdown",
223
+ start_index: int = 0,
224
+ max_length: int = 10000,
225
+ block_media: bool = True,
226
+ ) -> str:
227
+ """
228
+ Fetch and extract the main text content from any webpage. Fully executes JavaScript to load React/SPAs and aggressively strips images/media (including base64) to save context tokens.
229
+
230
+ Args:
231
+ url: The full URL of the webpage to fetch (must start with http/https).
232
+ format: Output format. Set to "markdown", "clean_html", or "raw_html". Default is "markdown" (highly recommended to save context tokens).
233
+ start_index: Character offset to start reading from for pagination. Use this if a document is too large to fit in the context window. Default is 0.
234
+ max_length: Maximum characters to return per request. Default is 10000.
235
+ block_media: Block images, videos, and fonts entirely at the network layer to drastically speed up page loads and dodge tracking pixels. Default is True.
236
+ """
237
+ try:
238
+ browser = await browser_manager.get_browser()
239
+ context = await browser.new_context()
240
+ page = await context.new_page()
241
+
242
+ if block_media:
243
+
244
+ async def route_handler(route):
245
+ if route.request.resource_type in ["image", "media", "font"]:
246
+ await route.abort()
247
+ else:
248
+ await route.continue_()
249
+
250
+ await page.route("**/*", route_handler)
251
+
252
+ try:
253
+ await page.goto(url, wait_until="networkidle", timeout=15000)
254
+ except PlaywrightTimeoutError:
255
+ pass
256
+
257
+ content = await page.content()
258
+ await context.close()
259
+
260
+ if format == "raw_html":
261
+ final_content = content
262
+ else:
263
+ soup = BeautifulSoup(content, "html.parser")
264
+
265
+ for tag in soup(
266
+ [
267
+ "script",
268
+ "style",
269
+ "nav",
270
+ "header",
271
+ "footer",
272
+ "noscript",
273
+ "iframe",
274
+ "svg",
275
+ "aside",
276
+ ".advertisement",
277
+ "img",
278
+ "picture",
279
+ "video",
280
+ "audio",
281
+ "canvas",
282
+ "map",
283
+ "area",
284
+ "dialog",
285
+ ]
286
+ ):
287
+ tag.decompose()
288
+
289
+ for tag in soup.find_all(True):
290
+ if isinstance(tag, Tag):
291
+ tag.attrs.pop("style", None)
292
+ src = _get_string_attr(tag, "src")
293
+ if src.startswith("data:image"):
294
+ tag.attrs.pop("src", None)
295
+
296
+ if format == "clean_html":
297
+ final_content = str(soup)
298
+ else:
299
+ final_content = md(str(soup), heading_style="ATX")
300
+ final_content = re.sub(r"\n{3,}", "\n\n", final_content).strip()
301
+
302
+ total_length = len(final_content)
303
+ paginated_text = final_content[start_index : start_index + max_length]
304
+
305
+ metadata = f"\n\n---\n[Document Info: Showing characters {start_index} to {start_index + len(paginated_text)} of {total_length} total."
306
+ if start_index + max_length < total_length:
307
+ metadata += f" Use start_index={start_index + max_length} to read more."
308
+ metadata += "]"
309
+
310
+ return paginated_text + metadata
311
+
312
+ except Exception as e:
313
+ return f"An error occurred while fetching the URL: {str(e)}"
314
+
315
+
316
+ def main():
317
+ mcp.run(transport="stdio")
318
+
319
+
320
+ if __name__ == "__main__":
321
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+