searchfetch 3.3.0__tar.gz → 3.3.2__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.
- {searchfetch-3.3.0/searchfetch.egg-info → searchfetch-3.3.2}/PKG-INFO +5 -5
- {searchfetch-3.3.0 → searchfetch-3.3.2}/pyproject.toml +7 -18
- {searchfetch-3.3.0 → searchfetch-3.3.2/searchfetch.egg-info}/PKG-INFO +5 -5
- searchfetch-3.3.2/searchfetch.egg-info/requires.txt +8 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/server.py +113 -44
- searchfetch-3.3.0/searchfetch.egg-info/requires.txt +0 -8
- {searchfetch-3.3.0 → searchfetch-3.3.2}/LICENSE +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/MANIFEST.in +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/README.md +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/searchfetch.egg-info/SOURCES.txt +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/searchfetch.egg-info/dependency_links.txt +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/searchfetch.egg-info/entry_points.txt +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/searchfetch.egg-info/top_level.txt +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/setup.cfg +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/crates-package.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/devto.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/docker-hub.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/docs-page.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/docs-rs.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/duckduckgo-search.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/github-issue.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/github-repo.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/gitlab.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/go-pkg.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/google-search.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/javadoc.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/mdn-web-docs.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/npm-package.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/pypi-package.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/raw.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/reddit.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/wikipedia.json +0 -0
- {searchfetch-3.3.0 → searchfetch-3.3.2}/templates/youtube.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: searchfetch
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
4
|
Summary: A maximum fault-tolerant, stealth-enabled MCP server for web searching and fetching.
|
|
5
5
|
Author: Max
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,13 +10,13 @@ Keywords: mcp,search,fetch,llm,ai,agent,stealth
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: mcp>=
|
|
14
|
-
Requires-Dist: cloakbrowser>=0.
|
|
13
|
+
Requires-Dist: mcp>=2.0.0
|
|
14
|
+
Requires-Dist: cloakbrowser>=0.5.2
|
|
15
15
|
Requires-Dist: beautifulsoup4>=4.15.0
|
|
16
|
-
Requires-Dist: markdownify>=1.2.
|
|
16
|
+
Requires-Dist: markdownify>=1.2.3
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
19
|
-
Requires-Dist: ruff>=0.
|
|
19
|
+
Requires-Dist: ruff>=0.16.0; extra == "dev"
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
22
|
# SearchFetch (MCP Server)
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=83.0.0"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "searchfetch"
|
|
7
|
-
version = "3.3.
|
|
7
|
+
version = "3.3.2"
|
|
8
8
|
description = "A maximum fault-tolerant, stealth-enabled MCP server for web searching and fetching."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -14,16 +14,16 @@ authors =[
|
|
|
14
14
|
]
|
|
15
15
|
keywords =["mcp", "search", "fetch", "llm", "ai", "agent", "stealth"]
|
|
16
16
|
dependencies =[
|
|
17
|
-
"mcp>=
|
|
18
|
-
"cloakbrowser>=0.
|
|
17
|
+
"mcp>=2.0.0",
|
|
18
|
+
"cloakbrowser>=0.5.2",
|
|
19
19
|
"beautifulsoup4>=4.15.0",
|
|
20
|
-
"markdownify>=1.2.
|
|
20
|
+
"markdownify>=1.2.3",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.optional-dependencies]
|
|
24
24
|
dev = [
|
|
25
25
|
"pytest>=9.1.1",
|
|
26
|
-
"ruff>=0.
|
|
26
|
+
"ruff>=0.16.0",
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
[project.urls]
|
|
@@ -38,18 +38,7 @@ searchfetch = "server:main"
|
|
|
38
38
|
py-modules =["server"]
|
|
39
39
|
|
|
40
40
|
[tool.setuptools.data-files]
|
|
41
|
-
templates = [
|
|
42
|
-
"templates/github-repo.json",
|
|
43
|
-
"templates/github-issue.json",
|
|
44
|
-
"templates/npm-package.json",
|
|
45
|
-
"templates/pypi-package.json",
|
|
46
|
-
"templates/crates-package.json",
|
|
47
|
-
"templates/google-search.json",
|
|
48
|
-
"templates/duckduckgo-search.json",
|
|
49
|
-
"templates/docs-page.json",
|
|
50
|
-
"templates/docs-rs.json",
|
|
51
|
-
"templates/docker-hub.json",
|
|
52
|
-
]
|
|
41
|
+
templates = ["templates/*.json"]
|
|
53
42
|
|
|
54
43
|
[tool.ruff]
|
|
55
44
|
line-length = 100
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: searchfetch
|
|
3
|
-
Version: 3.3.
|
|
3
|
+
Version: 3.3.2
|
|
4
4
|
Summary: A maximum fault-tolerant, stealth-enabled MCP server for web searching and fetching.
|
|
5
5
|
Author: Max
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,13 +10,13 @@ Keywords: mcp,search,fetch,llm,ai,agent,stealth
|
|
|
10
10
|
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
|
-
Requires-Dist: mcp>=
|
|
14
|
-
Requires-Dist: cloakbrowser>=0.
|
|
13
|
+
Requires-Dist: mcp>=2.0.0
|
|
14
|
+
Requires-Dist: cloakbrowser>=0.5.2
|
|
15
15
|
Requires-Dist: beautifulsoup4>=4.15.0
|
|
16
|
-
Requires-Dist: markdownify>=1.2.
|
|
16
|
+
Requires-Dist: markdownify>=1.2.3
|
|
17
17
|
Provides-Extra: dev
|
|
18
18
|
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
19
|
-
Requires-Dist: ruff>=0.
|
|
19
|
+
Requires-Dist: ruff>=0.16.0; extra == "dev"
|
|
20
20
|
Dynamic: license-file
|
|
21
21
|
|
|
22
22
|
# SearchFetch (MCP Server)
|
|
@@ -8,12 +8,12 @@ from pathlib import Path
|
|
|
8
8
|
from bs4 import BeautifulSoup, Tag
|
|
9
9
|
from cloakbrowser import launch_async
|
|
10
10
|
from markdownify import markdownify as md
|
|
11
|
-
from mcp.server
|
|
11
|
+
from mcp.server import MCPServer
|
|
12
12
|
from playwright.async_api import TimeoutError as PlaywrightTimeoutError
|
|
13
13
|
|
|
14
|
-
__version__ = "3.3.
|
|
14
|
+
__version__ = "3.3.2"
|
|
15
15
|
|
|
16
|
-
mcp =
|
|
16
|
+
mcp = MCPServer("searchfetch", version=__version__)
|
|
17
17
|
|
|
18
18
|
# ---------------------------------------------------------------------------
|
|
19
19
|
# Browser Manager — single shared browser instance, stealth by default
|
|
@@ -448,13 +448,16 @@ ACCESS_DENIED_TITLE_PATTERNS = [
|
|
|
448
448
|
]
|
|
449
449
|
|
|
450
450
|
ACCESS_DENIED_BODY_TRIGGERS = [
|
|
451
|
-
"captcha",
|
|
452
451
|
"verify you are human",
|
|
453
452
|
"are you a robot",
|
|
454
|
-
"unusual traffic",
|
|
455
453
|
"access denied",
|
|
454
|
+
]
|
|
455
|
+
|
|
456
|
+
ACCESS_DENIED_STRONG_BODY_TRIGGERS = [
|
|
457
|
+
"our systems have detected unusual traffic",
|
|
456
458
|
"sorry, you have been blocked",
|
|
457
459
|
"to continue, please type the characters",
|
|
460
|
+
"bots use duckduckgo too",
|
|
458
461
|
]
|
|
459
462
|
|
|
460
463
|
_ACCESS_DENIED_RE = re.compile("|".join(ACCESS_DENIED_TITLE_PATTERNS), re.IGNORECASE)
|
|
@@ -473,8 +476,10 @@ def _detect_access_failure(http_status: int | None, soup: BeautifulSoup) -> bool
|
|
|
473
476
|
body = soup.find("body")
|
|
474
477
|
if body:
|
|
475
478
|
body_text = body.get_text(" ", strip=True)
|
|
479
|
+
lowered = body_text.lower()
|
|
480
|
+
if any(trigger in lowered for trigger in ACCESS_DENIED_STRONG_BODY_TRIGGERS):
|
|
481
|
+
return True
|
|
476
482
|
if len(body_text) < 800:
|
|
477
|
-
lowered = body_text.lower()
|
|
478
483
|
if any(trigger in lowered for trigger in ACCESS_DENIED_BODY_TRIGGERS):
|
|
479
484
|
return True
|
|
480
485
|
|
|
@@ -606,6 +611,7 @@ async def fetch_html(url: str, template: dict | None, block_media: bool, retry:
|
|
|
606
611
|
or "econnreset" in exc_msg
|
|
607
612
|
or "enotfound" in exc_msg
|
|
608
613
|
or "navigation" in exc_msg
|
|
614
|
+
or "navigating" in exc_msg
|
|
609
615
|
)
|
|
610
616
|
if attempt < attempts - 1 and is_network:
|
|
611
617
|
await asyncio.sleep(0.5)
|
|
@@ -1033,30 +1039,10 @@ def generic_fallback(html_content: str, _url: str = "") -> str:
|
|
|
1033
1039
|
# ---------------------------------------------------------------------------
|
|
1034
1040
|
|
|
1035
1041
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
engine: str = "duckduckgo",
|
|
1040
|
-
region: str | None = None,
|
|
1041
|
-
safe_search: bool | None = None,
|
|
1042
|
-
max_results: int = 10,
|
|
1043
|
-
block_media: bool = True,
|
|
1044
|
-
) -> str:
|
|
1045
|
-
"""
|
|
1046
|
-
Search the web using template-driven search engines (Google, DuckDuckGo, or custom).
|
|
1047
|
-
|
|
1048
|
-
Args:
|
|
1049
|
-
query: The search query string.
|
|
1050
|
-
engine: Search engine: 'duckduckgo' or 'google'. Also accepts custom template names.
|
|
1051
|
-
region: Region code. DDG: 'us-en', 'de-de', etc. Google: 'de-de' maps to hl=de, gl=de.
|
|
1052
|
-
safe_search: Enable safe search. Maps to engine-specific params.
|
|
1053
|
-
max_results: Max results to extract. Default: 10.
|
|
1054
|
-
block_media: Block images/media/fonts. Default: True.
|
|
1055
|
-
"""
|
|
1056
|
-
# 1. Resolve engine → template name
|
|
1042
|
+
def _resolve_search_candidate(
|
|
1043
|
+
engine: str, query: str, region: str | None, safe_search: bool | None
|
|
1044
|
+
) -> tuple[str, dict, str]:
|
|
1057
1045
|
template_name = resolve_search_template(engine)
|
|
1058
|
-
|
|
1059
|
-
# 2. Resolve the template object
|
|
1060
1046
|
if template_name.startswith("{"):
|
|
1061
1047
|
try:
|
|
1062
1048
|
template = json.loads(template_name)
|
|
@@ -1070,33 +1056,116 @@ async def websearch(
|
|
|
1070
1056
|
)
|
|
1071
1057
|
template = BUILTIN_TEMPLATES[template_name]
|
|
1072
1058
|
|
|
1073
|
-
# Validate url_template exists
|
|
1074
1059
|
if not template.get("url_template"):
|
|
1075
1060
|
raise ValueError(
|
|
1076
1061
|
f"Template '{template.get('name', template_name)}' is not a search "
|
|
1077
1062
|
"template (no url_template). Use webfetch for page templates."
|
|
1078
1063
|
)
|
|
1079
1064
|
|
|
1080
|
-
# 3. Map universal params to engine-specific url_params
|
|
1081
1065
|
engine_key = engine if engine in ("duckduckgo", "google") else "custom"
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
# 4. Resolve URL template
|
|
1085
|
-
resolved_url = resolve_url_template(template, url_params)
|
|
1086
|
-
|
|
1087
|
-
# Google safe_search: append &safe=active after URL resolution
|
|
1066
|
+
params = map_search_params(query, engine_key, region, safe_search)
|
|
1067
|
+
resolved_url = resolve_url_template(template, params)
|
|
1088
1068
|
if engine_key == "google" and safe_search is True:
|
|
1089
1069
|
resolved_url += "&safe=active"
|
|
1070
|
+
return engine, template, resolved_url
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _resolve_duckduckgo_lite_candidate(
|
|
1074
|
+
query: str, region: str | None, safe_search: bool | None
|
|
1075
|
+
) -> tuple[str, dict, str]:
|
|
1076
|
+
template = dict(BUILTIN_TEMPLATES["duckduckgo-search"])
|
|
1077
|
+
template.update(
|
|
1078
|
+
{
|
|
1079
|
+
"name": "duckduckgo-lite-search",
|
|
1080
|
+
"url_template": "https://lite.duckduckgo.com/lite/?q={query}&kl={kl}&kp={kp}",
|
|
1081
|
+
"sections": [
|
|
1082
|
+
{
|
|
1083
|
+
"name": "Results",
|
|
1084
|
+
"selector": "a.result-link",
|
|
1085
|
+
"multiple": True,
|
|
1086
|
+
"max_items": 10,
|
|
1087
|
+
"children": [
|
|
1088
|
+
{"name": "Title", "selector": "", "format": "text"},
|
|
1089
|
+
{
|
|
1090
|
+
"name": "URL",
|
|
1091
|
+
"selector": "",
|
|
1092
|
+
"format": "attribute",
|
|
1093
|
+
"attribute": "href",
|
|
1094
|
+
"transform": "decode_ddg_url",
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
}
|
|
1098
|
+
],
|
|
1099
|
+
}
|
|
1100
|
+
)
|
|
1101
|
+
params = map_search_params(query, "duckduckgo", region, safe_search)
|
|
1102
|
+
return "duckduckgo-lite", template, resolve_url_template(template, params)
|
|
1090
1103
|
|
|
1091
|
-
# 5. Fetch
|
|
1092
|
-
html = await fetch_html(resolved_url, template, block_media, retry=True)
|
|
1093
1104
|
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1105
|
+
def _resolve_search_candidates(
|
|
1106
|
+
engine: str, query: str, region: str | None, safe_search: bool | None
|
|
1107
|
+
) -> list[tuple[str, dict, str]]:
|
|
1108
|
+
primary = _resolve_search_candidate(engine, query, region, safe_search)
|
|
1109
|
+
if engine == "google":
|
|
1110
|
+
return [
|
|
1111
|
+
primary,
|
|
1112
|
+
_resolve_search_candidate("duckduckgo", query, region, safe_search),
|
|
1113
|
+
_resolve_duckduckgo_lite_candidate(query, region, safe_search),
|
|
1114
|
+
]
|
|
1115
|
+
if engine == "duckduckgo":
|
|
1116
|
+
return [primary, _resolve_duckduckgo_lite_candidate(query, region, safe_search)]
|
|
1117
|
+
return [primary]
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
@mcp.tool()
|
|
1121
|
+
async def websearch(
|
|
1122
|
+
query: str,
|
|
1123
|
+
engine: str = "duckduckgo",
|
|
1124
|
+
region: str | None = None,
|
|
1125
|
+
safe_search: bool | None = None,
|
|
1126
|
+
max_results: int = 10,
|
|
1127
|
+
block_media: bool = True,
|
|
1128
|
+
) -> str:
|
|
1129
|
+
"""
|
|
1130
|
+
Search the web using template-driven search engines (Google, DuckDuckGo, or custom).
|
|
1131
|
+
|
|
1132
|
+
Args:
|
|
1133
|
+
query: The search query string.
|
|
1134
|
+
engine: Search engine: 'duckduckgo' or 'google'. Also accepts custom template names.
|
|
1135
|
+
region: Region code. DDG: 'us-en', 'de-de', etc. Google: 'de-de' maps to hl=de, gl=de.
|
|
1136
|
+
safe_search: Enable safe search. Maps to engine-specific params.
|
|
1137
|
+
max_results: Max results to extract. Default: 10.
|
|
1138
|
+
block_media: Block images/media/fonts. Default: True.
|
|
1139
|
+
"""
|
|
1140
|
+
candidates = _resolve_search_candidates(engine, query, region, safe_search)
|
|
1141
|
+
last_exc = None
|
|
1142
|
+
|
|
1143
|
+
for selected_engine, template, resolved_url in candidates:
|
|
1144
|
+
try:
|
|
1145
|
+
html = await fetch_html(resolved_url, template, block_media, retry=True)
|
|
1146
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
1147
|
+
sections_data = extract_template(
|
|
1148
|
+
soup, template, origin="", max_results_override=max_results
|
|
1149
|
+
)
|
|
1150
|
+
result_section = next(
|
|
1151
|
+
(section for section in sections_data if section.get("type") == "search_results"),
|
|
1152
|
+
None,
|
|
1153
|
+
)
|
|
1154
|
+
if result_section and result_section.get("items"):
|
|
1155
|
+
result = compose_search_results(
|
|
1156
|
+
sections_data, template.get("name", selected_engine)
|
|
1157
|
+
)
|
|
1158
|
+
if selected_engine != engine:
|
|
1159
|
+
result = (
|
|
1160
|
+
f"[websearch: {engine} was unavailable; results are from "
|
|
1161
|
+
f"{selected_engine}.]\n\n{result}"
|
|
1162
|
+
)
|
|
1163
|
+
return result
|
|
1164
|
+
last_exc = RuntimeError(f"No results extracted from {selected_engine}.")
|
|
1165
|
+
except Exception as exc:
|
|
1166
|
+
last_exc = exc
|
|
1097
1167
|
|
|
1098
|
-
|
|
1099
|
-
return compose_search_results(sections_data, template.get("name", template_name))
|
|
1168
|
+
raise last_exc or RuntimeError("No search results found.")
|
|
1100
1169
|
|
|
1101
1170
|
|
|
1102
1171
|
@mcp.tool()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|