internal-web-reader 0.1.0__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,3 @@
1
+ """Internal Web Reader — MCP Server for Claude Code."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,129 @@
1
+ """MCP Server 入口。Cookie 懒加载:首次访问某域名时自动从浏览器提取。"""
2
+
3
+ import logging
4
+ from urllib.parse import urlparse
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from .cookies import get_cookie_header
9
+ from .reader import read_page
10
+
11
+ logging.basicConfig(
12
+ level=logging.INFO,
13
+ format="[web-reader] %(message)s",
14
+ handlers=[logging.StreamHandler()],
15
+ )
16
+ log = logging.getLogger("internal_web_reader")
17
+
18
+ # domain → cookie header string(内存缓存)
19
+ _cookie_cache: dict[str, str] = {}
20
+
21
+ mcp = FastMCP("internal-web-reader")
22
+
23
+
24
+ def _get_cookie(url: str) -> str | None:
25
+ """获取 URL 对应的 Cookie,没有就自动从浏览器提取并缓存。"""
26
+ try:
27
+ host = urlparse(url).hostname or ""
28
+ except Exception:
29
+ return None
30
+
31
+ if not host:
32
+ return None
33
+
34
+ # 1. 缓存命中
35
+ if host in _cookie_cache:
36
+ return _cookie_cache[host]
37
+
38
+ # 2. 父域名
39
+ parts = host.split(".")
40
+ for i in range(1, len(parts) - 1):
41
+ parent = ".".join(parts[i:])
42
+ if parent in _cookie_cache:
43
+ return _cookie_cache[parent]
44
+
45
+ # 3. 自动提取
46
+ header = get_cookie_header(host)
47
+ if header:
48
+ _cookie_cache[host] = header
49
+ return header
50
+
51
+
52
+ def _refresh_cookie(host: str) -> str | None:
53
+ """强制刷新某域名的 Cookie(401/403 时调用)。"""
54
+ _cookie_cache.pop(host, None)
55
+ parts = host.split(".")
56
+ for i in range(1, len(parts) - 1):
57
+ _cookie_cache.pop(".".join(parts[i:]), None)
58
+
59
+ header = get_cookie_header(host)
60
+ if header:
61
+ _cookie_cache[host] = header
62
+ return header
63
+
64
+
65
+ # ── Tools ────────────────────────────────────────────────
66
+
67
+
68
+ @mcp.tool()
69
+ def read_page_tool(url: str) -> str:
70
+ """读取任意网页,返回干净的 Markdown。
71
+ Cookie 自动从浏览器提取,不需要预配置。
72
+ 支持内网 GitLab、Wiki、文档中心等。
73
+ """
74
+ cookie = _get_cookie(url)
75
+
76
+ try:
77
+ page = read_page(url, cookie=cookie)
78
+ except Exception as e:
79
+ # 401/403 → 刷新 Cookie 重试
80
+ msg = str(e)
81
+ if any(k in msg for k in ("401", "403", "Unauthorized", "Forbidden")):
82
+ try:
83
+ host = urlparse(url).hostname or ""
84
+ except Exception:
85
+ raise
86
+ log.info(f"鉴权失败,刷新 {host} Cookie...")
87
+ cookie = _refresh_cookie(host)
88
+ if cookie:
89
+ log.info("重试中...")
90
+ page = read_page(url, cookie=cookie)
91
+ else:
92
+ raise
93
+ else:
94
+ raise
95
+
96
+ lines = [f"# {page.title}", f"> {page.url}", "", page.content]
97
+
98
+ if 0 < len(page.links) <= 30:
99
+ lines += ["", "---", "## 页面链接"]
100
+ for lk in page.links:
101
+ lines.append(f"- [{lk['text']}]({lk['href']})")
102
+
103
+ return "\n".join(lines)
104
+
105
+
106
+ @mcp.tool()
107
+ def list_links(url: str) -> str:
108
+ """列出页面上的所有链接,用于浏览内网站点。"""
109
+ cookie = _get_cookie(url)
110
+ page = read_page(url, cookie=cookie)
111
+
112
+ if not page.links:
113
+ return f"页面 {url} 上未找到链接。"
114
+
115
+ lines = [f"# {page.title} 的链接", f"> {len(page.links)} 个", ""]
116
+ for lk in page.links:
117
+ lines.append(f"- [{lk['text']}]({lk['href']})")
118
+ return "\n".join(lines)
119
+
120
+
121
+ # ── Entry ────────────────────────────────────────────────
122
+
123
+
124
+ def main():
125
+ mcp.run()
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
@@ -0,0 +1,44 @@
1
+ """从本地浏览器自动提取 Cookie。"""
2
+
3
+ import logging
4
+
5
+ log = logging.getLogger("internal_web_reader")
6
+
7
+
8
+ def get_cookie_header(domain: str) -> str | None:
9
+ """
10
+ 从 Chrome / Edge / Firefox 提取指定域名的 Cookie,
11
+ 返回 "name1=val1; name2=val2" 格式的字符串。
12
+ 找不到返回 None。
13
+ """
14
+ cookie_jars = []
15
+
16
+ # 依次尝试各浏览器
17
+ try:
18
+ import browser_cookie3
19
+
20
+ for loader in (browser_cookie3.chrome, browser_cookie3.edge, browser_cookie3.firefox):
21
+ try:
22
+ cj = loader(domain_name=domain)
23
+ cookie_jars.append(cj)
24
+ except Exception as e:
25
+ log.debug(f"{loader.__name__} 跳过: {e}")
26
+ except ImportError:
27
+ log.warning("browser_cookie3 未安装,无法自动提取 Cookie")
28
+ return None
29
+
30
+ # 合并 + 去重
31
+ seen: dict[str, str] = {}
32
+ for cj in cookie_jars:
33
+ for c in cj:
34
+ # 匹配: 精确域名 或 父域名 (.example.com 匹配 sub.example.com)
35
+ cdomain = c.domain.lstrip(".")
36
+ if domain == cdomain or domain.endswith("." + cdomain):
37
+ seen[c.name] = c.value
38
+
39
+ if not seen:
40
+ return None
41
+
42
+ header = "; ".join(f"{k}={v}" for k, v in seen.items())
43
+ log.info(f"从浏览器提取 {len(seen)} 个 Cookie ({domain})")
44
+ return header
@@ -0,0 +1,114 @@
1
+ """抓取网页,HTML → 干净 Markdown。"""
2
+
3
+ import logging
4
+ from dataclasses import dataclass, field
5
+ from urllib.parse import urljoin
6
+
7
+ import httpx
8
+ from bs4 import BeautifulSoup
9
+ import html2text
10
+
11
+ log = logging.getLogger("internal_web_reader")
12
+
13
+ _H2T = html2text.HTML2Text()
14
+ _H2T.body_width = 0 # 不自动换行
15
+ _H2T.ignore_links = False
16
+ _H2T.ignore_images = False
17
+ _H2T.protect_links = True
18
+ _H2T.wrap_links = False
19
+ _H2T.unicode_snob = True
20
+
21
+ _UA = (
22
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
23
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
24
+ "Chrome/124.0.0.0 Safari/537.36"
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class PageResult:
30
+ url: str
31
+ title: str
32
+ content: str
33
+ links: list[dict] = field(default_factory=list)
34
+
35
+
36
+ def read_page(url: str, cookie: str | None = None, timeout: float = 30) -> PageResult:
37
+ """抓取一个 URL,返回清洗后的 Markdown。"""
38
+
39
+ headers = {
40
+ "User-Agent": _UA,
41
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
42
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
43
+ }
44
+ if cookie:
45
+ headers["Cookie"] = cookie
46
+
47
+ resp = httpx.get(url, headers=headers, follow_redirects=True, timeout=timeout)
48
+ resp.raise_for_status()
49
+
50
+ ct = resp.headers.get("content-type", "")
51
+ if "html" not in ct and "text" not in ct:
52
+ return PageResult(url=url, title=url, content=f"[非 HTML] Content-Type: {ct}")
53
+
54
+ # 检测编码
55
+ resp.encoding = resp.charset_encoding or "utf-8"
56
+ html = resp.text
57
+
58
+ soup = BeautifulSoup(html, "html.parser")
59
+
60
+ # 标题
61
+ title = ""
62
+ if soup.title and soup.title.string:
63
+ title = soup.title.string.strip()
64
+ if not title:
65
+ h1 = soup.find("h1")
66
+ if h1:
67
+ title = h1.get_text(strip=True)
68
+ title = title or url
69
+
70
+ # 去噪
71
+ for tag in soup.find_all(["script", "style", "noscript", "iframe", "svg", "nav", "footer", "aside"]):
72
+ tag.decompose()
73
+
74
+ # 找主体内容
75
+ main = None
76
+ for sel in ("article", "main", '[role="main"]', ".content", ".article-content", ".markdown-body", "#content", "#main"):
77
+ el = soup.select_one(sel)
78
+ if el and len(el.get_text(strip=True)) > 100:
79
+ main = el
80
+ break
81
+
82
+ target = main or soup.body or soup
83
+
84
+ # 提取链接
85
+ links = _extract_links(target, url)
86
+
87
+ # HTML → Markdown
88
+ content_html = str(target)
89
+ markdown = _H2T.handle(content_html).strip()
90
+
91
+ # 截断
92
+ if len(markdown) > 200_000:
93
+ markdown = markdown[:200_000] + "\n\n... [内容过长,已截断]"
94
+
95
+ return PageResult(url=url, title=title, content=markdown, links=links)
96
+
97
+
98
+ def _extract_links(soup, base_url: str) -> list[dict]:
99
+ seen: set[str] = set()
100
+ links: list[dict] = []
101
+ for a in soup.find_all("a", href=True):
102
+ href = a["href"].strip()
103
+ if not href or href.startswith("#") or href.startswith("javascript:") or href.startswith("mailto:"):
104
+ continue
105
+ try:
106
+ absolute = urljoin(base_url, href)
107
+ except Exception:
108
+ continue
109
+ if absolute in seen:
110
+ continue
111
+ seen.add(absolute)
112
+ text = a.get_text(strip=True)[:100] or href
113
+ links.append({"text": text, "href": absolute})
114
+ return links
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: internal-web-reader
3
+ Version: 0.1.0
4
+ Summary: MCP Server - let Claude Code read internal web pages via browser cookies
5
+ Author: zhanghaoran18006
6
+ License-Expression: MIT
7
+ Keywords: mcp,claude,internal,wiki,confluence,gitlab
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Libraries
12
+ Requires-Python: >=3.10
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: mcp[cli]<2.0.0,>=1.2.0
15
+ Requires-Dist: httpx>=0.27.0
16
+ Requires-Dist: beautifulsoup4>=4.12.0
17
+ Requires-Dist: html2text>=2024.2.26
18
+ Requires-Dist: browser_cookie3>=0.19.1
19
+
20
+ # Internal Web Reader
21
+
22
+ 让 Claude Code 能读内网网页。
23
+
24
+ ## 安装
25
+
26
+ ```
27
+ pip install internal-web-reader
28
+ ```
29
+
30
+ ## 配置 Claude Code
31
+
32
+ ```
33
+ claude mcp add -s user internal-web-reader -- internal-web-reader
34
+ ```
35
+
36
+ ## 使用
37
+
38
+ 在 Claude Code 里直接给链接:
39
+
40
+ ```
41
+ > 看看 https://git.corpautohome.com/xxx/-/issues/123
42
+ > 读一下 https://doc.autohome.com.cn/docapi/page/share/share_1Hx8joJJIfo
43
+ ```
44
+
45
+ Cookie 自动从浏览器提取。前提:浏览器里登录过、VPN 连着。
@@ -0,0 +1,9 @@
1
+ internal_web_reader/__init__.py,sha256=EUOrBX7nMn6Rt-PgPHWBEjuORtXuWfQuuey2hNFzFT8,81
2
+ internal_web_reader/__main__.py,sha256=YxSIfjaCroshuvjqaSqcZG-7iRzYnVOn_gwLBuMOjak,3628
3
+ internal_web_reader/cookies.py,sha256=3HEA1fSjor_BqrnntRpQlLKUERCuBewy9sJRoPl7pFI,1363
4
+ internal_web_reader/reader.py,sha256=t1hmIMRONSyBHftghpqkCbM-PPt0Z6uGNdGJgxQzG70,3246
5
+ internal_web_reader-0.1.0.dist-info/METADATA,sha256=hifEi16FrM3aTJ3m8zzUGDy2J5n-W3yYkc8NGGF-_zs,1211
6
+ internal_web_reader-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
7
+ internal_web_reader-0.1.0.dist-info/entry_points.txt,sha256=iEhJm-03JedWF3i1Awbegyg1u2-YEF_TYC0QGORzV24,74
8
+ internal_web_reader-0.1.0.dist-info/top_level.txt,sha256=jhE9xwuy3_-TQU3BWs3xkH32SA_1bYFDEA1ArtYGnZ8,20
9
+ internal_web_reader-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ internal-web-reader = internal_web_reader.__main__:main
@@ -0,0 +1 @@
1
+ internal_web_reader