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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: internal-web-reader
3
- Version: 0.2.2
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://doc.autohome.com.cn/docapi/page/share/share_1Hx8joJJIfo
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://doc.autohome.com.cn/docapi/page/share/share_1Hx8joJJIfo
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.2.2"
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.sync_api import sync_playwright
52
+ from playwright.async_api import async_playwright
53
53
 
54
- _pw = sync_playwright().start()
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 _is_login_page(page) -> bool:
69
- url_lower = page.url.lower()
70
- login_keywords = ["/login", "/signin", "/sso", "/cas", "/oauth", "/auth", "passport"]
71
- if any(k in url_lower for k in login_keywords):
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, timeout_ms: int = 300_000):
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
- page.evaluate("""() => {
89
- const banner = document.createElement('div');
90
- banner.id = '__iwr_login_hint__';
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
- banner.textContent = 'Internal Web Reader: Please log in here, then wait a few seconds...';
96
- document.body.prepend(banner);
87
+ b.textContent = 'Internal Web Reader: Please log in here. Auto-continuing after login...';
88
+ document.body.prepend(b);
97
89
  }""")
98
90
 
99
- # 等待:密码框消失 + URL 不再包含 login 关键词
100
- import time
101
- start = time.time()
102
- while (time.time() - start) * 1000 < timeout_ms:
103
- time.sleep(2)
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
- # 如果 URL 不再含 login 关键词 且 没有密码框 → 登录成功
108
- login_in_url = any(k in current_url for k in ["/login", "/signin", "/sso", "/cas", "/oauth", "passport"])
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(" 登录等待超时(5分钟),尝试继续读取当前页面...")
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 _is_login_page(page):
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: internal-web-reader
3
- Version: 0.2.2
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://doc.autohome.com.cn/docapi/page/share/share_1Hx8joJJIfo
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()