internal-web-reader 0.2.2__tar.gz → 0.3.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.
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/PKG-INFO +2 -2
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/README.md +1 -1
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/pyproject.toml +1 -1
- internal_web_reader-0.3.0/src/internal_web_reader/__main__.py +54 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader/reader.py +40 -55
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/PKG-INFO +2 -2
- internal_web_reader-0.2.2/src/internal_web_reader/__main__.py +0 -56
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/setup.cfg +0 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader/__init__.py +0 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/SOURCES.txt +0 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/dependency_links.txt +0 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/entry_points.txt +0 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/requires.txt +0 -0
- {internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: internal-web-reader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP Server - let Claude Code read internal web pages via browser cookies
|
|
5
5
|
Author: zhanghaoran18006
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ claude mcp add -s user internal-web-reader -- internal-web-reader
|
|
|
37
37
|
在 Claude Code 里给链接:
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
> 看看 https://
|
|
40
|
+
> 看看 https://wiki.yourcompany.com/pages/xxx
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
第一次访问需要登录的网站,会弹出 Chrome 窗口。登录一下,之后就不用再登了。
|
|
@@ -19,7 +19,7 @@ claude mcp add -s user internal-web-reader -- internal-web-reader
|
|
|
19
19
|
在 Claude Code 里给链接:
|
|
20
20
|
|
|
21
21
|
```
|
|
22
|
-
> 看看 https://
|
|
22
|
+
> 看看 https://wiki.yourcompany.com/pages/xxx
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
第一次访问需要登录的网站,会弹出 Chrome 窗口。登录一下,之后就不用再登了。
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "internal-web-reader"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "MCP Server - let Claude Code read internal web pages via browser cookies"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""MCP Server 入口。"""
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
8
|
+
from .reader import read_page, close_browser
|
|
9
|
+
|
|
10
|
+
logging.basicConfig(
|
|
11
|
+
level=logging.INFO,
|
|
12
|
+
format="[web-reader] %(message)s",
|
|
13
|
+
handlers=[logging.StreamHandler()],
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
mcp = FastMCP("internal-web-reader")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
async def read_web_page(url: str) -> str:
|
|
21
|
+
"""[READ WEB PAGES] Open any URL in a local Chrome browser and return Markdown content.
|
|
22
|
+
USE THIS TOOL for all web page reading, especially internal/intranet sites
|
|
23
|
+
(GitLab, Wiki, docs requiring VPN/SSO). Do NOT use the built-in Fetch tool for web pages.
|
|
24
|
+
First visit to a login-required site will pop up a browser window for you to log in.
|
|
25
|
+
Session is saved automatically for future visits.
|
|
26
|
+
"""
|
|
27
|
+
page = await read_page(url)
|
|
28
|
+
|
|
29
|
+
lines = [f"# {page.title}", f"> {page.url}", "", page.content]
|
|
30
|
+
if 0 < len(page.links) <= 30:
|
|
31
|
+
lines += ["", "---", "## Links"]
|
|
32
|
+
for lk in page.links:
|
|
33
|
+
lines.append(f"- [{lk['text']}]({lk['href']})")
|
|
34
|
+
return "\n".join(lines)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@mcp.tool()
|
|
38
|
+
async def list_web_links(url: str) -> str:
|
|
39
|
+
"""List all links on a web page for navigation."""
|
|
40
|
+
page = await read_page(url)
|
|
41
|
+
if not page.links:
|
|
42
|
+
return f"No links found on {url}."
|
|
43
|
+
lines = [f"# {page.title} - Links", f"> {len(page.links)} links", ""]
|
|
44
|
+
for lk in page.links:
|
|
45
|
+
lines.append(f"- [{lk['text']}]({lk['href']})")
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
mcp.run()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
"""用 Playwright 启动浏览器读网页。登录一次,session 自动保存。"""
|
|
1
|
+
"""用 Playwright (async) 启动浏览器读网页。登录一次,session 自动保存。"""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
import os
|
|
5
6
|
import sys
|
|
@@ -39,22 +40,21 @@ _pw = None
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
def _stderr(msg: str):
|
|
42
|
-
"""写 stderr,保证在 MCP stdio 模式下用户能看到提示。"""
|
|
43
43
|
sys.stderr.write(f"\n{'='*60}\n{msg}\n{'='*60}\n")
|
|
44
44
|
sys.stderr.flush()
|
|
45
45
|
|
|
46
46
|
|
|
47
|
-
def _ensure_browser():
|
|
47
|
+
async def _ensure_browser():
|
|
48
48
|
global _context, _pw
|
|
49
49
|
if _context is not None:
|
|
50
50
|
return _context
|
|
51
51
|
|
|
52
|
-
from playwright.
|
|
52
|
+
from playwright.async_api import async_playwright
|
|
53
53
|
|
|
54
|
-
_pw =
|
|
54
|
+
_pw = await async_playwright().start()
|
|
55
55
|
os.makedirs(_USER_DATA_DIR, exist_ok=True)
|
|
56
56
|
|
|
57
|
-
_context = _pw.chromium.launch_persistent_context(
|
|
57
|
+
_context = await _pw.chromium.launch_persistent_context(
|
|
58
58
|
_USER_DATA_DIR,
|
|
59
59
|
headless=False,
|
|
60
60
|
channel="chrome",
|
|
@@ -65,80 +65,65 @@ def _ensure_browser():
|
|
|
65
65
|
return _context
|
|
66
66
|
|
|
67
67
|
|
|
68
|
-
def
|
|
69
|
-
url_lower =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return True
|
|
73
|
-
if page.query_selector('input[type="password"]'):
|
|
74
|
-
return True
|
|
75
|
-
return False
|
|
68
|
+
def _is_login_url(url: str) -> bool:
|
|
69
|
+
url_lower = url.lower()
|
|
70
|
+
keywords = ["/login", "/signin", "/sso", "/cas", "/oauth", "/auth", "passport"]
|
|
71
|
+
return any(k in url_lower for k in keywords)
|
|
76
72
|
|
|
77
73
|
|
|
78
|
-
def _wait_for_login(page,
|
|
79
|
-
"""
|
|
80
|
-
检测到登录页后,在页面上注入提示,并等待用户完成登录。
|
|
81
|
-
"""
|
|
74
|
+
async def _wait_for_login(page, timeout_s: int = 300):
|
|
82
75
|
_stderr(
|
|
83
76
|
" 需要登录!请切换到弹出的浏览器窗口完成登录。\n"
|
|
84
77
|
" 登录成功后会自动继续,不用回来操作终端。"
|
|
85
78
|
)
|
|
86
79
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999999;'
|
|
80
|
+
await page.evaluate("""() => {
|
|
81
|
+
const b = document.createElement('div');
|
|
82
|
+
b.id = '__iwr__';
|
|
83
|
+
b.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:999999;'
|
|
92
84
|
+ 'background:#ff6b35;color:white;padding:16px;text-align:center;'
|
|
93
85
|
+ 'font-size:18px;font-weight:bold;font-family:sans-serif;'
|
|
94
86
|
+ 'box-shadow:0 4px 12px rgba(0,0,0,0.3);';
|
|
95
|
-
|
|
96
|
-
document.body.prepend(
|
|
87
|
+
b.textContent = 'Internal Web Reader: Please log in here. Auto-continuing after login...';
|
|
88
|
+
document.body.prepend(b);
|
|
97
89
|
}""")
|
|
98
90
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
elapsed = 0
|
|
92
|
+
while elapsed < timeout_s:
|
|
93
|
+
await asyncio.sleep(2)
|
|
94
|
+
elapsed += 2
|
|
95
|
+
|
|
104
96
|
current_url = page.url.lower()
|
|
105
|
-
has_pw = page.query_selector('input[type="password"]')
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if not login_in_url and not has_pw:
|
|
110
|
-
# 移除提示
|
|
111
|
-
page.evaluate("""() => {
|
|
112
|
-
const el = document.getElementById('__iwr_login_hint__');
|
|
113
|
-
if (el) el.remove();
|
|
114
|
-
}""")
|
|
97
|
+
has_pw = await page.query_selector('input[type="password"]')
|
|
98
|
+
|
|
99
|
+
if not _is_login_url(current_url) and not has_pw:
|
|
100
|
+
await page.evaluate("() => { const e = document.getElementById('__iwr__'); if(e) e.remove(); }")
|
|
115
101
|
_stderr(" 登录成功!正在读取页面...")
|
|
116
|
-
page.wait_for_load_state("domcontentloaded", timeout=30_000)
|
|
102
|
+
await page.wait_for_load_state("domcontentloaded", timeout=30_000)
|
|
117
103
|
return True
|
|
118
104
|
|
|
119
|
-
_stderr("
|
|
105
|
+
_stderr(" 登录等待超时,尝试继续...")
|
|
120
106
|
return False
|
|
121
107
|
|
|
122
108
|
|
|
123
|
-
def read_page(url: str, timeout: float = 60) -> PageResult:
|
|
124
|
-
ctx = _ensure_browser()
|
|
125
|
-
page = ctx.pages[0] if ctx.pages else ctx.new_page()
|
|
109
|
+
async def read_page(url: str, timeout: float = 60) -> PageResult:
|
|
110
|
+
ctx = await _ensure_browser()
|
|
111
|
+
page = ctx.pages[0] if ctx.pages else await ctx.new_page()
|
|
126
112
|
|
|
127
|
-
# 让浏览器窗口获得焦点
|
|
128
113
|
try:
|
|
129
|
-
page.bring_to_front()
|
|
114
|
+
await page.bring_to_front()
|
|
130
115
|
except Exception:
|
|
131
116
|
pass
|
|
132
117
|
|
|
133
|
-
page.goto(url, wait_until="domcontentloaded", timeout=timeout * 1000)
|
|
118
|
+
await page.goto(url, wait_until="domcontentloaded", timeout=timeout * 1000)
|
|
134
119
|
|
|
135
120
|
# 登录检测
|
|
136
|
-
if
|
|
137
|
-
_wait_for_login(page)
|
|
121
|
+
if _is_login_url(page.url) or await page.query_selector('input[type="password"]'):
|
|
122
|
+
await _wait_for_login(page)
|
|
138
123
|
|
|
139
124
|
final_url = page.url
|
|
140
|
-
html = page.content()
|
|
141
|
-
title = page.title() or url
|
|
125
|
+
html = await page.content()
|
|
126
|
+
title = await page.title() or url
|
|
142
127
|
|
|
143
128
|
soup = BeautifulSoup(html, "html.parser")
|
|
144
129
|
|
|
@@ -162,16 +147,16 @@ def read_page(url: str, timeout: float = 60) -> PageResult:
|
|
|
162
147
|
return PageResult(url=final_url, title=title, content=markdown, links=links)
|
|
163
148
|
|
|
164
149
|
|
|
165
|
-
def close_browser():
|
|
150
|
+
async def close_browser():
|
|
166
151
|
global _context, _pw
|
|
167
152
|
if _context:
|
|
168
153
|
try:
|
|
169
|
-
_context.close()
|
|
154
|
+
await _context.close()
|
|
170
155
|
except Exception:
|
|
171
156
|
pass
|
|
172
157
|
if _pw:
|
|
173
158
|
try:
|
|
174
|
-
_pw.stop()
|
|
159
|
+
await _pw.stop()
|
|
175
160
|
except Exception:
|
|
176
161
|
pass
|
|
177
162
|
_context = None
|
{internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: internal-web-reader
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: MCP Server - let Claude Code read internal web pages via browser cookies
|
|
5
5
|
Author: zhanghaoran18006
|
|
6
6
|
License-Expression: MIT
|
|
@@ -37,7 +37,7 @@ claude mcp add -s user internal-web-reader -- internal-web-reader
|
|
|
37
37
|
在 Claude Code 里给链接:
|
|
38
38
|
|
|
39
39
|
```
|
|
40
|
-
> 看看 https://
|
|
40
|
+
> 看看 https://wiki.yourcompany.com/pages/xxx
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
第一次访问需要登录的网站,会弹出 Chrome 窗口。登录一下,之后就不用再登了。
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
"""MCP Server 入口。用 Playwright 浏览器读网页,登录一次自动保存。"""
|
|
2
|
-
|
|
3
|
-
import atexit
|
|
4
|
-
import logging
|
|
5
|
-
|
|
6
|
-
from mcp.server.fastmcp import FastMCP
|
|
7
|
-
|
|
8
|
-
from .reader import read_page, close_browser
|
|
9
|
-
|
|
10
|
-
logging.basicConfig(
|
|
11
|
-
level=logging.INFO,
|
|
12
|
-
format="[web-reader] %(message)s",
|
|
13
|
-
handlers=[logging.StreamHandler()],
|
|
14
|
-
)
|
|
15
|
-
log = logging.getLogger("internal_web_reader")
|
|
16
|
-
|
|
17
|
-
atexit.register(close_browser)
|
|
18
|
-
|
|
19
|
-
mcp = FastMCP("internal-web-reader")
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
@mcp.tool()
|
|
23
|
-
def read_web_page(url: str) -> str:
|
|
24
|
-
"""【读网页用这个】通过本地浏览器打开任意 URL 并返回 Markdown 内容。
|
|
25
|
-
专门用于访问公司内网页面(GitLab、Wiki、文档中心等需要 VPN 的网站)。
|
|
26
|
-
也适用于任何公开网页。首次访问需登录的站点会弹出浏览器窗口完成登录,
|
|
27
|
-
之后 session 自动保存。优先使用此工具而非内置 Fetch 来读取网页。
|
|
28
|
-
"""
|
|
29
|
-
page = read_page(url)
|
|
30
|
-
|
|
31
|
-
lines = [f"# {page.title}", f"> {page.url}", "", page.content]
|
|
32
|
-
if 0 < len(page.links) <= 30:
|
|
33
|
-
lines += ["", "---", "## 页面链接"]
|
|
34
|
-
for lk in page.links:
|
|
35
|
-
lines.append(f"- [{lk['text']}]({lk['href']})")
|
|
36
|
-
return "\n".join(lines)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
@mcp.tool()
|
|
40
|
-
def list_web_links(url: str) -> str:
|
|
41
|
-
"""列出网页上的所有链接,用于浏览内网站点结构。"""
|
|
42
|
-
page = read_page(url)
|
|
43
|
-
if not page.links:
|
|
44
|
-
return f"页面 {url} 上未找到链接。"
|
|
45
|
-
lines = [f"# {page.title} 的链接", f"> {len(page.links)} 个", ""]
|
|
46
|
-
for lk in page.links:
|
|
47
|
-
lines.append(f"- [{lk['text']}]({lk['href']})")
|
|
48
|
-
return "\n".join(lines)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def main():
|
|
52
|
-
mcp.run()
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if __name__ == "__main__":
|
|
56
|
-
main()
|
|
File without changes
|
|
File without changes
|
{internal_web_reader-0.2.2 → internal_web_reader-0.3.0}/src/internal_web_reader.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|