link-check-lite 1.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.
- link_check_lite-1.0.0/CHANGELOG.md +11 -0
- link_check_lite-1.0.0/LICENSE +9 -0
- link_check_lite-1.0.0/MANIFEST.in +3 -0
- link_check_lite-1.0.0/PKG-INFO +172 -0
- link_check_lite-1.0.0/README.md +142 -0
- link_check_lite-1.0.0/link_check_lite.egg-info/PKG-INFO +172 -0
- link_check_lite-1.0.0/link_check_lite.egg-info/SOURCES.txt +17 -0
- link_check_lite-1.0.0/link_check_lite.egg-info/dependency_links.txt +1 -0
- link_check_lite-1.0.0/link_check_lite.egg-info/entry_points.txt +3 -0
- link_check_lite-1.0.0/link_check_lite.egg-info/requires.txt +1 -0
- link_check_lite-1.0.0/link_check_lite.egg-info/top_level.txt +2 -0
- link_check_lite-1.0.0/link_check_lite.py +118 -0
- link_check_lite-1.0.0/link_check_lite_mcp.py +32 -0
- link_check_lite-1.0.0/llms.txt +19 -0
- link_check_lite-1.0.0/pyproject.toml +45 -0
- link_check_lite-1.0.0/server.json +24 -0
- link_check_lite-1.0.0/setup.cfg +4 -0
- link_check_lite-1.0.0/tests/test_lite.py +68 -0
- link_check_lite-1.0.0/tests/test_mcp.py +45 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
What changed in link-check-lite, newest first.
|
|
4
|
+
|
|
5
|
+
## 1.0.0 - 2026-11-04
|
|
6
|
+
|
|
7
|
+
- First release.
|
|
8
|
+
- Checks a text file of links and shows status codes, broken links and where redirects end up.
|
|
9
|
+
- Exit code 1 when any link is broken, so it works in cron jobs and CI.
|
|
10
|
+
- Free MCP server, `link-check-lite-mcp`, with one tool, `check_links`.
|
|
11
|
+
- You can install it from PyPI with `pip install link-check-lite`.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tidybyte
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: link-check-lite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Check a file of links for status codes and redirects. Has a CLI and an MCP server.
|
|
5
|
+
Author: Tidy Byte Tools
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://tidybyte.gumroad.com
|
|
8
|
+
Project-URL: Source, https://github.com/tidybytetools/link-check-lite
|
|
9
|
+
Project-URL: Issues, https://github.com/tidybytetools/link-check-lite/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/tidybytetools/link-check-lite/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: links,link-checker,broken-links,redirects,http,status-code,cli,mcp,mcp-server
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: mcp<2,>=1.2
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# link-check-lite
|
|
32
|
+
|
|
33
|
+
[](https://github.com/tidybytetools/link-check-lite/blob/main/LICENSE)
|
|
34
|
+
[](https://www.python.org/)
|
|
35
|
+
[](#use-it-from-claude-desktop-or-cursor-mcp)
|
|
36
|
+
|
|
37
|
+
Free tool that checks a list of links. It tells you which ones work, which are broken, and where redirects end up.
|
|
38
|
+
Need sitemaps, speed checks, sketchy-domain warnings or CSV reports? The paid version does that: **[link-check on Gumroad](https://tidybyte.gumroad.com)**
|
|
39
|
+
|
|
40
|
+
MIT license. Python 3.10 or newer, on Windows, Mac and Linux. The checker itself uses only Python's built-in tools.
|
|
41
|
+
|
|
42
|
+
<!-- mcp-name: io.github.tidybytetools/link-check-lite -->
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install link-check-lite
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or use pipx, which keeps it in its own space:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pipx install link-check-lite
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Want the newest code from GitHub instead?
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
pip install git+https://github.com/tidybytetools/link-check-lite
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Use it
|
|
63
|
+
|
|
64
|
+
Put your links in a text file, one per line. Lines that start with `#` are skipped.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
link-check-lite links.txt
|
|
68
|
+
link-check-lite links.txt -w 3 --timeout 20 # gentler and more patient
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Example run:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
OK 200 https://example.com/
|
|
75
|
+
OK 200 https://httpbin.org/redirect/2 -> https://httpbin.org/get (2 redirects)
|
|
76
|
+
FAIL 404 https://example.com/old-page
|
|
77
|
+
FAIL --- https://no-such-site.example ([Errno -2] Name or service not known)
|
|
78
|
+
|
|
79
|
+
4 checked, 2 ok, 2 broken
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Exit code is 0 when every link works and 1 when any link is broken, so it works in a cron job or CI.
|
|
83
|
+
|
|
84
|
+
When you run it in a terminal it ends with one short line about the paid version. `--quiet` turns it off, and it never shows when output is piped.
|
|
85
|
+
|
|
86
|
+
## Known limits
|
|
87
|
+
|
|
88
|
+
- **Some sites block bots.** They may answer 403 or 429 even when the page opens fine in a browser.
|
|
89
|
+
- **No JavaScript.** Redirects done with JavaScript are not followed.
|
|
90
|
+
- **Text file only.** No CSV input, no sitemaps, and results print to the screen.
|
|
91
|
+
- **No robots.txt and no per-site rate limit.** It checks 5 links at a time with a small pause. Don't point it at thousands of pages on one site.
|
|
92
|
+
- **Not a safety check.** It does not tell you if a link is a scam.
|
|
93
|
+
|
|
94
|
+
## Lite vs paid
|
|
95
|
+
|
|
96
|
+
| | lite (free) | [link-check](https://tidybyte.gumroad.com) |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Status codes and broken links | yes | yes |
|
|
99
|
+
| Redirect chain and final URL | yes | yes |
|
|
100
|
+
| Text file input | yes | yes |
|
|
101
|
+
| CSV input and sitemap URLs (with sitemap index) | no | yes |
|
|
102
|
+
| Response time and slow-link flag | no | yes |
|
|
103
|
+
| http vs https checks | no | yes |
|
|
104
|
+
| Look-alike and sketchy domain warnings (local rules) | no | yes |
|
|
105
|
+
| robots.txt and one-hit-per-second per site | no | yes |
|
|
106
|
+
| CSV, JSON and Markdown reports | no | yes |
|
|
107
|
+
|
|
108
|
+
## How it compares to other free tools
|
|
109
|
+
|
|
110
|
+
All of these are free. Pick what fits you.
|
|
111
|
+
|
|
112
|
+
| | link-check-lite | [lychee](https://github.com/lycheeverse/lychee) | [linkchecker](https://github.com/linkchecker/linkchecker) | [muffet](https://github.com/raviqqe/muffet) |
|
|
113
|
+
|---|---|---|---|---|
|
|
114
|
+
| Checks a plain list of links | yes | yes | no, it crawls | no, it crawls |
|
|
115
|
+
| Crawls a whole site | no | no | yes | yes |
|
|
116
|
+
| Install | pip | binary / cargo | pip | binary / go |
|
|
117
|
+
| Built-in MCP server for AI apps | yes | no | no | no |
|
|
118
|
+
| Lots of options | no | yes | yes | yes |
|
|
119
|
+
|
|
120
|
+
If you need to crawl a big site or check links inside Markdown files, lychee and linkchecker are great.
|
|
121
|
+
|
|
122
|
+
## Use it from Claude Desktop or Cursor (MCP)
|
|
123
|
+
|
|
124
|
+
There is a small free MCP server built in. It gives your AI app one tool, `check_links`. It needs [uv](https://docs.astral.sh/uv/).
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
uvx --from link-check-lite link-check-lite-mcp
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Claude Desktop (`claude_desktop_config.json`) or Cursor (`~/.cursor/mcp.json`):
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"link-check-lite": {
|
|
136
|
+
"command": "uvx",
|
|
137
|
+
"args": ["--from", "link-check-lite", "link-check-lite-mcp"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Tool: `check_links(urls, timeout=10.0)`. Send up to 200 links. It returns one result per link.
|
|
144
|
+
|
|
145
|
+
## Use it from Python
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from link_check_lite import check_link
|
|
149
|
+
print(check_link("https://example.com"))
|
|
150
|
+
# {'url': 'https://example.com', 'status': 200, 'ok': True, 'final_url': 'https://example.com', 'redirects': [], 'error': None}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## For AI agents
|
|
154
|
+
|
|
155
|
+
- **Use when:** you need to know if web links work and where their redirects end up.
|
|
156
|
+
- **Don't use when:** you need to know if a link is safe, need a whole site crawled, or need speed or https checks. Use the paid version for those.
|
|
157
|
+
- **Input:** a list of http(s) URLs (max 200 per MCP call), plus an optional `timeout` in seconds.
|
|
158
|
+
- **Output:** a list of `{url, status, ok, final_url, redirects, error}`. `status` is null when the link could not connect. `ok` is true for 200-399. `redirects` lists each hop as `"CODE url"`.
|
|
159
|
+
- CLI exit code is 0 when all links work and 1 when any link is broken or on error. See also [`llms.txt`](https://github.com/tidybytetools/link-check-lite/blob/main/llms.txt).
|
|
160
|
+
|
|
161
|
+
## Tests
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
pip install "mcp>=1.2,<2"
|
|
165
|
+
python3 -m unittest discover tests
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The tests use a tiny local web server, so they don't need the internet.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# link-check-lite
|
|
2
|
+
|
|
3
|
+
[](https://github.com/tidybytetools/link-check-lite/blob/main/LICENSE)
|
|
4
|
+
[](https://www.python.org/)
|
|
5
|
+
[](#use-it-from-claude-desktop-or-cursor-mcp)
|
|
6
|
+
|
|
7
|
+
Free tool that checks a list of links. It tells you which ones work, which are broken, and where redirects end up.
|
|
8
|
+
Need sitemaps, speed checks, sketchy-domain warnings or CSV reports? The paid version does that: **[link-check on Gumroad](https://tidybyte.gumroad.com)**
|
|
9
|
+
|
|
10
|
+
MIT license. Python 3.10 or newer, on Windows, Mac and Linux. The checker itself uses only Python's built-in tools.
|
|
11
|
+
|
|
12
|
+
<!-- mcp-name: io.github.tidybytetools/link-check-lite -->
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
pip install link-check-lite
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or use pipx, which keeps it in its own space:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
pipx install link-check-lite
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Want the newest code from GitHub instead?
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
pip install git+https://github.com/tidybytetools/link-check-lite
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Use it
|
|
33
|
+
|
|
34
|
+
Put your links in a text file, one per line. Lines that start with `#` are skipped.
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
link-check-lite links.txt
|
|
38
|
+
link-check-lite links.txt -w 3 --timeout 20 # gentler and more patient
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Example run:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
OK 200 https://example.com/
|
|
45
|
+
OK 200 https://httpbin.org/redirect/2 -> https://httpbin.org/get (2 redirects)
|
|
46
|
+
FAIL 404 https://example.com/old-page
|
|
47
|
+
FAIL --- https://no-such-site.example ([Errno -2] Name or service not known)
|
|
48
|
+
|
|
49
|
+
4 checked, 2 ok, 2 broken
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Exit code is 0 when every link works and 1 when any link is broken, so it works in a cron job or CI.
|
|
53
|
+
|
|
54
|
+
When you run it in a terminal it ends with one short line about the paid version. `--quiet` turns it off, and it never shows when output is piped.
|
|
55
|
+
|
|
56
|
+
## Known limits
|
|
57
|
+
|
|
58
|
+
- **Some sites block bots.** They may answer 403 or 429 even when the page opens fine in a browser.
|
|
59
|
+
- **No JavaScript.** Redirects done with JavaScript are not followed.
|
|
60
|
+
- **Text file only.** No CSV input, no sitemaps, and results print to the screen.
|
|
61
|
+
- **No robots.txt and no per-site rate limit.** It checks 5 links at a time with a small pause. Don't point it at thousands of pages on one site.
|
|
62
|
+
- **Not a safety check.** It does not tell you if a link is a scam.
|
|
63
|
+
|
|
64
|
+
## Lite vs paid
|
|
65
|
+
|
|
66
|
+
| | lite (free) | [link-check](https://tidybyte.gumroad.com) |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| Status codes and broken links | yes | yes |
|
|
69
|
+
| Redirect chain and final URL | yes | yes |
|
|
70
|
+
| Text file input | yes | yes |
|
|
71
|
+
| CSV input and sitemap URLs (with sitemap index) | no | yes |
|
|
72
|
+
| Response time and slow-link flag | no | yes |
|
|
73
|
+
| http vs https checks | no | yes |
|
|
74
|
+
| Look-alike and sketchy domain warnings (local rules) | no | yes |
|
|
75
|
+
| robots.txt and one-hit-per-second per site | no | yes |
|
|
76
|
+
| CSV, JSON and Markdown reports | no | yes |
|
|
77
|
+
|
|
78
|
+
## How it compares to other free tools
|
|
79
|
+
|
|
80
|
+
All of these are free. Pick what fits you.
|
|
81
|
+
|
|
82
|
+
| | link-check-lite | [lychee](https://github.com/lycheeverse/lychee) | [linkchecker](https://github.com/linkchecker/linkchecker) | [muffet](https://github.com/raviqqe/muffet) |
|
|
83
|
+
|---|---|---|---|---|
|
|
84
|
+
| Checks a plain list of links | yes | yes | no, it crawls | no, it crawls |
|
|
85
|
+
| Crawls a whole site | no | no | yes | yes |
|
|
86
|
+
| Install | pip | binary / cargo | pip | binary / go |
|
|
87
|
+
| Built-in MCP server for AI apps | yes | no | no | no |
|
|
88
|
+
| Lots of options | no | yes | yes | yes |
|
|
89
|
+
|
|
90
|
+
If you need to crawl a big site or check links inside Markdown files, lychee and linkchecker are great.
|
|
91
|
+
|
|
92
|
+
## Use it from Claude Desktop or Cursor (MCP)
|
|
93
|
+
|
|
94
|
+
There is a small free MCP server built in. It gives your AI app one tool, `check_links`. It needs [uv](https://docs.astral.sh/uv/).
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
uvx --from link-check-lite link-check-lite-mcp
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Claude Desktop (`claude_desktop_config.json`) or Cursor (`~/.cursor/mcp.json`):
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"link-check-lite": {
|
|
106
|
+
"command": "uvx",
|
|
107
|
+
"args": ["--from", "link-check-lite", "link-check-lite-mcp"]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Tool: `check_links(urls, timeout=10.0)`. Send up to 200 links. It returns one result per link.
|
|
114
|
+
|
|
115
|
+
## Use it from Python
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from link_check_lite import check_link
|
|
119
|
+
print(check_link("https://example.com"))
|
|
120
|
+
# {'url': 'https://example.com', 'status': 200, 'ok': True, 'final_url': 'https://example.com', 'redirects': [], 'error': None}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## For AI agents
|
|
124
|
+
|
|
125
|
+
- **Use when:** you need to know if web links work and where their redirects end up.
|
|
126
|
+
- **Don't use when:** you need to know if a link is safe, need a whole site crawled, or need speed or https checks. Use the paid version for those.
|
|
127
|
+
- **Input:** a list of http(s) URLs (max 200 per MCP call), plus an optional `timeout` in seconds.
|
|
128
|
+
- **Output:** a list of `{url, status, ok, final_url, redirects, error}`. `status` is null when the link could not connect. `ok` is true for 200-399. `redirects` lists each hop as `"CODE url"`.
|
|
129
|
+
- CLI exit code is 0 when all links work and 1 when any link is broken or on error. See also [`llms.txt`](https://github.com/tidybytetools/link-check-lite/blob/main/llms.txt).
|
|
130
|
+
|
|
131
|
+
## Tests
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
pip install "mcp>=1.2,<2"
|
|
135
|
+
python3 -m unittest discover tests
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The tests use a tiny local web server, so they don't need the internet.
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: link-check-lite
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Check a file of links for status codes and redirects. Has a CLI and an MCP server.
|
|
5
|
+
Author: Tidy Byte Tools
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://tidybyte.gumroad.com
|
|
8
|
+
Project-URL: Source, https://github.com/tidybytetools/link-check-lite
|
|
9
|
+
Project-URL: Issues, https://github.com/tidybytetools/link-check-lite/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/tidybytetools/link-check-lite/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: links,link-checker,broken-links,redirects,http,status-code,cli,mcp,mcp-server
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: mcp<2,>=1.2
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# link-check-lite
|
|
32
|
+
|
|
33
|
+
[](https://github.com/tidybytetools/link-check-lite/blob/main/LICENSE)
|
|
34
|
+
[](https://www.python.org/)
|
|
35
|
+
[](#use-it-from-claude-desktop-or-cursor-mcp)
|
|
36
|
+
|
|
37
|
+
Free tool that checks a list of links. It tells you which ones work, which are broken, and where redirects end up.
|
|
38
|
+
Need sitemaps, speed checks, sketchy-domain warnings or CSV reports? The paid version does that: **[link-check on Gumroad](https://tidybyte.gumroad.com)**
|
|
39
|
+
|
|
40
|
+
MIT license. Python 3.10 or newer, on Windows, Mac and Linux. The checker itself uses only Python's built-in tools.
|
|
41
|
+
|
|
42
|
+
<!-- mcp-name: io.github.tidybytetools/link-check-lite -->
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
pip install link-check-lite
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or use pipx, which keeps it in its own space:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
pipx install link-check-lite
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Want the newest code from GitHub instead?
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
pip install git+https://github.com/tidybytetools/link-check-lite
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Use it
|
|
63
|
+
|
|
64
|
+
Put your links in a text file, one per line. Lines that start with `#` are skipped.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
link-check-lite links.txt
|
|
68
|
+
link-check-lite links.txt -w 3 --timeout 20 # gentler and more patient
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Example run:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
OK 200 https://example.com/
|
|
75
|
+
OK 200 https://httpbin.org/redirect/2 -> https://httpbin.org/get (2 redirects)
|
|
76
|
+
FAIL 404 https://example.com/old-page
|
|
77
|
+
FAIL --- https://no-such-site.example ([Errno -2] Name or service not known)
|
|
78
|
+
|
|
79
|
+
4 checked, 2 ok, 2 broken
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Exit code is 0 when every link works and 1 when any link is broken, so it works in a cron job or CI.
|
|
83
|
+
|
|
84
|
+
When you run it in a terminal it ends with one short line about the paid version. `--quiet` turns it off, and it never shows when output is piped.
|
|
85
|
+
|
|
86
|
+
## Known limits
|
|
87
|
+
|
|
88
|
+
- **Some sites block bots.** They may answer 403 or 429 even when the page opens fine in a browser.
|
|
89
|
+
- **No JavaScript.** Redirects done with JavaScript are not followed.
|
|
90
|
+
- **Text file only.** No CSV input, no sitemaps, and results print to the screen.
|
|
91
|
+
- **No robots.txt and no per-site rate limit.** It checks 5 links at a time with a small pause. Don't point it at thousands of pages on one site.
|
|
92
|
+
- **Not a safety check.** It does not tell you if a link is a scam.
|
|
93
|
+
|
|
94
|
+
## Lite vs paid
|
|
95
|
+
|
|
96
|
+
| | lite (free) | [link-check](https://tidybyte.gumroad.com) |
|
|
97
|
+
|---|---|---|
|
|
98
|
+
| Status codes and broken links | yes | yes |
|
|
99
|
+
| Redirect chain and final URL | yes | yes |
|
|
100
|
+
| Text file input | yes | yes |
|
|
101
|
+
| CSV input and sitemap URLs (with sitemap index) | no | yes |
|
|
102
|
+
| Response time and slow-link flag | no | yes |
|
|
103
|
+
| http vs https checks | no | yes |
|
|
104
|
+
| Look-alike and sketchy domain warnings (local rules) | no | yes |
|
|
105
|
+
| robots.txt and one-hit-per-second per site | no | yes |
|
|
106
|
+
| CSV, JSON and Markdown reports | no | yes |
|
|
107
|
+
|
|
108
|
+
## How it compares to other free tools
|
|
109
|
+
|
|
110
|
+
All of these are free. Pick what fits you.
|
|
111
|
+
|
|
112
|
+
| | link-check-lite | [lychee](https://github.com/lycheeverse/lychee) | [linkchecker](https://github.com/linkchecker/linkchecker) | [muffet](https://github.com/raviqqe/muffet) |
|
|
113
|
+
|---|---|---|---|---|
|
|
114
|
+
| Checks a plain list of links | yes | yes | no, it crawls | no, it crawls |
|
|
115
|
+
| Crawls a whole site | no | no | yes | yes |
|
|
116
|
+
| Install | pip | binary / cargo | pip | binary / go |
|
|
117
|
+
| Built-in MCP server for AI apps | yes | no | no | no |
|
|
118
|
+
| Lots of options | no | yes | yes | yes |
|
|
119
|
+
|
|
120
|
+
If you need to crawl a big site or check links inside Markdown files, lychee and linkchecker are great.
|
|
121
|
+
|
|
122
|
+
## Use it from Claude Desktop or Cursor (MCP)
|
|
123
|
+
|
|
124
|
+
There is a small free MCP server built in. It gives your AI app one tool, `check_links`. It needs [uv](https://docs.astral.sh/uv/).
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
uvx --from link-check-lite link-check-lite-mcp
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Claude Desktop (`claude_desktop_config.json`) or Cursor (`~/.cursor/mcp.json`):
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"link-check-lite": {
|
|
136
|
+
"command": "uvx",
|
|
137
|
+
"args": ["--from", "link-check-lite", "link-check-lite-mcp"]
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Tool: `check_links(urls, timeout=10.0)`. Send up to 200 links. It returns one result per link.
|
|
144
|
+
|
|
145
|
+
## Use it from Python
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from link_check_lite import check_link
|
|
149
|
+
print(check_link("https://example.com"))
|
|
150
|
+
# {'url': 'https://example.com', 'status': 200, 'ok': True, 'final_url': 'https://example.com', 'redirects': [], 'error': None}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## For AI agents
|
|
154
|
+
|
|
155
|
+
- **Use when:** you need to know if web links work and where their redirects end up.
|
|
156
|
+
- **Don't use when:** you need to know if a link is safe, need a whole site crawled, or need speed or https checks. Use the paid version for those.
|
|
157
|
+
- **Input:** a list of http(s) URLs (max 200 per MCP call), plus an optional `timeout` in seconds.
|
|
158
|
+
- **Output:** a list of `{url, status, ok, final_url, redirects, error}`. `status` is null when the link could not connect. `ok` is true for 200-399. `redirects` lists each hop as `"CODE url"`.
|
|
159
|
+
- CLI exit code is 0 when all links work and 1 when any link is broken or on error. See also [`llms.txt`](https://github.com/tidybytetools/link-check-lite/blob/main/llms.txt).
|
|
160
|
+
|
|
161
|
+
## Tests
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
pip install "mcp>=1.2,<2"
|
|
165
|
+
python3 -m unittest discover tests
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The tests use a tiny local web server, so they don't need the internet.
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CHANGELOG.md
|
|
2
|
+
LICENSE
|
|
3
|
+
MANIFEST.in
|
|
4
|
+
README.md
|
|
5
|
+
link_check_lite.py
|
|
6
|
+
link_check_lite_mcp.py
|
|
7
|
+
llms.txt
|
|
8
|
+
pyproject.toml
|
|
9
|
+
server.json
|
|
10
|
+
link_check_lite.egg-info/PKG-INFO
|
|
11
|
+
link_check_lite.egg-info/SOURCES.txt
|
|
12
|
+
link_check_lite.egg-info/dependency_links.txt
|
|
13
|
+
link_check_lite.egg-info/entry_points.txt
|
|
14
|
+
link_check_lite.egg-info/requires.txt
|
|
15
|
+
link_check_lite.egg-info/top_level.txt
|
|
16
|
+
tests/test_lite.py
|
|
17
|
+
tests/test_mcp.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mcp<2,>=1.2
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""link-check-lite: check a file of links for status codes and redirects.
|
|
3
|
+
|
|
4
|
+
Free and MIT licensed. Paid version (sitemaps, CSV input, speed, https checks,
|
|
5
|
+
odd-domain warnings, robots.txt, CSV/JSON/Markdown reports):
|
|
6
|
+
https://tidybyte.gumroad.com
|
|
7
|
+
"""
|
|
8
|
+
import argparse, sys, time, urllib.error, urllib.parse, urllib.request
|
|
9
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
10
|
+
|
|
11
|
+
__version__ = "1.0.0"
|
|
12
|
+
PAID_URL = "https://tidybyte.gumroad.com"
|
|
13
|
+
UA = "link-check-lite/1.0 (+https://github.com/tidybytetools/link-check-lite)"
|
|
14
|
+
MAX_HOPS = 10
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _Stop(urllib.request.HTTPRedirectHandler):
|
|
18
|
+
def redirect_request(self, *a, **k):
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_opener = urllib.request.build_opener(_Stop)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _hit(url, method, timeout):
|
|
26
|
+
req = urllib.request.Request(url, method=method, headers={"User-Agent": UA})
|
|
27
|
+
try:
|
|
28
|
+
with _opener.open(req, timeout=timeout) as r:
|
|
29
|
+
return r.status, r.headers.get("Location"), None
|
|
30
|
+
except urllib.error.HTTPError as e:
|
|
31
|
+
return e.code, e.headers.get("Location"), None
|
|
32
|
+
except urllib.error.URLError as e:
|
|
33
|
+
return None, None, str(e.reason)
|
|
34
|
+
except Exception as e: # timeouts and odd errors
|
|
35
|
+
return None, None, str(e) or e.__class__.__name__
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_link(url, timeout=10.0):
|
|
39
|
+
"""Check one link. Returns {url, status, ok, final_url, redirects, error}.
|
|
40
|
+
|
|
41
|
+
redirects is a list of "CODE url" hops. ok is True for a final status 200-399.
|
|
42
|
+
"""
|
|
43
|
+
hops, cur, seen, err, code = [], url, set(), None, None
|
|
44
|
+
if urllib.parse.urlsplit(url).scheme not in ("http", "https"):
|
|
45
|
+
return {"url": url, "status": None, "ok": False, "final_url": url, "redirects": [], "error": "not an http(s) link"}
|
|
46
|
+
for _ in range(MAX_HOPS + 1):
|
|
47
|
+
code, loc, err = _hit(cur, "HEAD", timeout)
|
|
48
|
+
if code in (403, 405, 501):
|
|
49
|
+
code, loc, err = _hit(cur, "GET", timeout) # some servers refuse HEAD
|
|
50
|
+
if err or code not in (301, 302, 303, 307, 308) or not loc:
|
|
51
|
+
break
|
|
52
|
+
hops.append(f"{code} {cur}")
|
|
53
|
+
seen.add(cur)
|
|
54
|
+
cur = urllib.parse.urljoin(cur, loc)
|
|
55
|
+
if cur in seen:
|
|
56
|
+
err = "redirect loop"
|
|
57
|
+
break
|
|
58
|
+
else:
|
|
59
|
+
err = f"more than {MAX_HOPS} redirects"
|
|
60
|
+
ok = err is None and code is not None and 200 <= code < 400
|
|
61
|
+
return {"url": url, "status": code, "ok": ok, "final_url": cur, "redirects": hops, "error": err}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def read_links(path):
|
|
65
|
+
out = []
|
|
66
|
+
with open(path, encoding="utf-8-sig", errors="replace") as f:
|
|
67
|
+
for line in f:
|
|
68
|
+
line = line.strip()
|
|
69
|
+
if line and not line.startswith("#") and line not in out:
|
|
70
|
+
out.append(line if "://" in line else "https://" + line)
|
|
71
|
+
return out
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def check_links(urls, workers=5, timeout=10.0, delay=0.5):
|
|
75
|
+
"""Check many links, a few at a time, with a short pause between starts."""
|
|
76
|
+
def job(i_url):
|
|
77
|
+
i, u = i_url
|
|
78
|
+
time.sleep(i * delay / max(workers, 1))
|
|
79
|
+
return check_link(u, timeout)
|
|
80
|
+
with ThreadPoolExecutor(max_workers=max(1, workers)) as ex:
|
|
81
|
+
return list(ex.map(job, enumerate(urls)))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main(argv=None):
|
|
85
|
+
ap = argparse.ArgumentParser(prog="link-check-lite", description="Check a file of links for status codes and redirects.")
|
|
86
|
+
ap.add_argument("file", help="text file, one link per line (# lines are skipped)")
|
|
87
|
+
ap.add_argument("-w", "--workers", type=int, default=5, help="links at the same time (default 5)")
|
|
88
|
+
ap.add_argument("--timeout", type=float, default=10, help="seconds per link (default 10)")
|
|
89
|
+
ap.add_argument("-q", "--quiet", action="store_true", help="hide the one-line note about the paid version")
|
|
90
|
+
ap.add_argument("--version", action="version", version=__version__)
|
|
91
|
+
a = ap.parse_args(argv)
|
|
92
|
+
try:
|
|
93
|
+
urls = read_links(a.file)
|
|
94
|
+
except OSError as e:
|
|
95
|
+
print(f"error: {e}", file=sys.stderr)
|
|
96
|
+
return 1
|
|
97
|
+
if not urls:
|
|
98
|
+
print("error: no links found", file=sys.stderr)
|
|
99
|
+
return 1
|
|
100
|
+
rows = check_links(urls, a.workers, a.timeout)
|
|
101
|
+
bad = 0
|
|
102
|
+
for r in rows:
|
|
103
|
+
mark = "OK " if r["ok"] else "FAIL"
|
|
104
|
+
bad += not r["ok"]
|
|
105
|
+
line = f"{mark} {r['status'] or '---'} {r['url']}"
|
|
106
|
+
if r["redirects"]:
|
|
107
|
+
line += f" -> {r['final_url']} ({len(r['redirects'])} redirect{'s' * (len(r['redirects']) > 1)})"
|
|
108
|
+
if r["error"]:
|
|
109
|
+
line += f" ({r['error']})"
|
|
110
|
+
print(line)
|
|
111
|
+
print(f"\n{len(rows)} checked, {len(rows) - bad} ok, {bad} broken")
|
|
112
|
+
if not a.quiet and sys.stdout.isatty():
|
|
113
|
+
print(f"Need sitemaps, CSV/JSON reports, speed and sketchy-domain checks? {PAID_URL}")
|
|
114
|
+
return 1 if bad else 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
sys.exit(main())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Free MCP server for link-check-lite (stdio).
|
|
2
|
+
|
|
3
|
+
Run: uvx --from git+https://github.com/tidybytetools/link-check-lite link-check-lite-mcp
|
|
4
|
+
"""
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
import link_check_lite as lite
|
|
7
|
+
|
|
8
|
+
mcp = FastMCP("link-check-lite")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool()
|
|
12
|
+
def check_links(urls: list[str], timeout: float = 10.0) -> list[dict]:
|
|
13
|
+
"""Check web links and follow their redirects.
|
|
14
|
+
|
|
15
|
+
urls: up to 200 http(s) links.
|
|
16
|
+
timeout: seconds to wait for each link.
|
|
17
|
+
|
|
18
|
+
Returns one result per link: {url, status, ok, final_url, redirects, error}.
|
|
19
|
+
status is the HTTP code of the last hop (null if it could not connect).
|
|
20
|
+
ok is true for 200-399. redirects lists each hop as "CODE url".
|
|
21
|
+
"""
|
|
22
|
+
if len(urls) > 200:
|
|
23
|
+
raise ValueError("send 200 links or fewer per call")
|
|
24
|
+
return lite.check_links(list(urls), workers=5, timeout=timeout)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main():
|
|
28
|
+
mcp.run()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# link-check-lite
|
|
2
|
+
|
|
3
|
+
> Free, MIT-licensed tool that checks web links for their HTTP status code and follows redirects to the final URL. Has a CLI, a Python API and a stdio MCP server.
|
|
4
|
+
|
|
5
|
+
- Install: `pip install link-check-lite` (or from GitHub: `pip install git+https://github.com/tidybytetools/link-check-lite`)
|
|
6
|
+
- CLI: `link-check-lite links.txt` prints one line per link (OK/FAIL, status, final URL). Exit 0 if all ok, 1 if any broken or on error.
|
|
7
|
+
- Python: `from link_check_lite import check_link, check_links` returns dicts {url, status, ok, final_url, redirects, error}.
|
|
8
|
+
- MCP: `uvx --from link-check-lite link-check-lite-mcp` exposes `check_links(urls: list[str], timeout=10.0) -> list[dict]` (max 200 links per call).
|
|
9
|
+
|
|
10
|
+
## Use when
|
|
11
|
+
- You need to know if links work and where redirects end up.
|
|
12
|
+
|
|
13
|
+
## Don't use when
|
|
14
|
+
- You need sitemaps, CSV input, response times, http vs https checks, look-alike domain warnings, robots.txt handling or CSV/JSON/Markdown reports. The paid version does those: https://tidybyte.gumroad.com
|
|
15
|
+
- You need to know if a link is safe. This tool does not check for malware or phishing.
|
|
16
|
+
|
|
17
|
+
## Links
|
|
18
|
+
- [README](https://github.com/tidybytetools/link-check-lite/blob/main/README.md)
|
|
19
|
+
- [Paid version](https://tidybyte.gumroad.com)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "link-check-lite"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Check a file of links for status codes and redirects. Has a CLI and an MCP server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [{name = "Tidy Byte Tools"}]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"mcp>=1.2,<2",
|
|
16
|
+
]
|
|
17
|
+
keywords = ["links", "link-checker", "broken-links", "redirects", "http", "status-code", "cli", "mcp", "mcp-server"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Intended Audience :: End Users/Desktop",
|
|
23
|
+
"Operating System :: OS Independent",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
26
|
+
"Programming Language :: Python :: 3.10",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Internet :: WWW/HTTP :: Site Management :: Link Checking",
|
|
31
|
+
"Topic :: Utilities",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://tidybyte.gumroad.com"
|
|
36
|
+
Source = "https://github.com/tidybytetools/link-check-lite"
|
|
37
|
+
Issues = "https://github.com/tidybytetools/link-check-lite/issues"
|
|
38
|
+
Changelog = "https://github.com/tidybytetools/link-check-lite/blob/main/CHANGELOG.md"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
link-check-lite = "link_check_lite:main"
|
|
42
|
+
link-check-lite-mcp = "link_check_lite_mcp:main"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools]
|
|
45
|
+
py-modules = ["link_check_lite", "link_check_lite_mcp"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.tidybytetools/link-check-lite",
|
|
4
|
+
"title": "link-check-lite",
|
|
5
|
+
"description": "Check web links for status codes, broken links and redirect chains.",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/tidybytetools/link-check-lite",
|
|
8
|
+
"source": "github"
|
|
9
|
+
},
|
|
10
|
+
"websiteUrl": "https://github.com/tidybytetools/link-check-lite",
|
|
11
|
+
"version": "1.0.0",
|
|
12
|
+
"packages": [
|
|
13
|
+
{
|
|
14
|
+
"registryType": "pypi",
|
|
15
|
+
"registryBaseUrl": "https://pypi.org",
|
|
16
|
+
"identifier": "link-check-lite",
|
|
17
|
+
"version": "1.0.0",
|
|
18
|
+
"runtimeHint": "uvx",
|
|
19
|
+
"transport": {
|
|
20
|
+
"type": "stdio"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import io, os, sys, tempfile, threading, unittest
|
|
2
|
+
from contextlib import redirect_stdout
|
|
3
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
4
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
5
|
+
import link_check_lite as lite
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class H(BaseHTTPRequestHandler):
|
|
9
|
+
def log_message(self, *a): pass
|
|
10
|
+
def _send(self, code, loc=None):
|
|
11
|
+
self.send_response(code)
|
|
12
|
+
if loc: self.send_header("Location", loc)
|
|
13
|
+
self.send_header("Content-Length", "2"); self.end_headers()
|
|
14
|
+
if self.command == "GET": self.wfile.write(b"hi")
|
|
15
|
+
def route(self):
|
|
16
|
+
p = self.path
|
|
17
|
+
if p == "/ok": return self._send(200)
|
|
18
|
+
if p == "/a": return self._send(301, "/b")
|
|
19
|
+
if p == "/b": return self._send(302, "/ok")
|
|
20
|
+
if p == "/loop": return self._send(302, "/loop")
|
|
21
|
+
if p == "/nohead": return self._send(405 if self.command == "HEAD" else 200)
|
|
22
|
+
return self._send(404)
|
|
23
|
+
do_GET = do_HEAD = route
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class T(unittest.TestCase):
|
|
27
|
+
@classmethod
|
|
28
|
+
def setUpClass(cls):
|
|
29
|
+
cls.srv = ThreadingHTTPServer(("127.0.0.1", 0), H)
|
|
30
|
+
threading.Thread(target=cls.srv.serve_forever, daemon=True).start()
|
|
31
|
+
cls.b = f"http://127.0.0.1:{cls.srv.server_port}"
|
|
32
|
+
@classmethod
|
|
33
|
+
def tearDownClass(cls): cls.srv.shutdown(); cls.srv.server_close()
|
|
34
|
+
|
|
35
|
+
def test_ok(self):
|
|
36
|
+
r = lite.check_link(self.b + "/ok")
|
|
37
|
+
self.assertEqual((r["status"], r["ok"], r["redirects"]), (200, True, []))
|
|
38
|
+
def test_404(self):
|
|
39
|
+
r = lite.check_link(self.b + "/gone")
|
|
40
|
+
self.assertEqual((r["status"], r["ok"]), (404, False))
|
|
41
|
+
def test_redirects(self):
|
|
42
|
+
r = lite.check_link(self.b + "/a")
|
|
43
|
+
self.assertEqual(r["final_url"], self.b + "/ok"); self.assertEqual(len(r["redirects"]), 2)
|
|
44
|
+
self.assertTrue(r["redirects"][0].startswith("301 ")); self.assertTrue(r["ok"])
|
|
45
|
+
def test_loop(self):
|
|
46
|
+
r = lite.check_link(self.b + "/loop")
|
|
47
|
+
self.assertEqual(r["error"], "redirect loop"); self.assertFalse(r["ok"])
|
|
48
|
+
def test_head_refused(self):
|
|
49
|
+
self.assertEqual(lite.check_link(self.b + "/nohead")["status"], 200)
|
|
50
|
+
def test_cannot_connect(self):
|
|
51
|
+
r = lite.check_link("http://127.0.0.1:1/x", timeout=2)
|
|
52
|
+
self.assertIsNone(r["status"]); self.assertTrue(r["error"])
|
|
53
|
+
def test_not_http(self):
|
|
54
|
+
self.assertEqual(lite.check_link("mailto:a@b.c")["error"], "not an http(s) link")
|
|
55
|
+
def test_cli(self):
|
|
56
|
+
d = tempfile.mkdtemp(); p = os.path.join(d, "l.txt")
|
|
57
|
+
with open(p, "w") as f: f.write(f"# list\n{self.b}/ok\n{self.b}/a\n{self.b}/ok\n")
|
|
58
|
+
self.assertEqual(lite.read_links(p), [self.b + "/ok", self.b + "/a"])
|
|
59
|
+
buf = io.StringIO()
|
|
60
|
+
with redirect_stdout(buf): code = lite.main([p])
|
|
61
|
+
self.assertEqual(code, 0); self.assertIn("2 checked, 2 ok, 0 broken", buf.getvalue())
|
|
62
|
+
self.assertNotIn("gumroad", buf.getvalue()) # no upsell when not a terminal
|
|
63
|
+
with open(p, "a") as f: f.write(f"{self.b}/gone\n")
|
|
64
|
+
with redirect_stdout(io.StringIO()): self.assertEqual(lite.main([p]), 1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
unittest.main()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import asyncio, os, sys, threading, unittest
|
|
2
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
3
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
4
|
+
from test_lite import H
|
|
5
|
+
try:
|
|
6
|
+
import mcp # noqa
|
|
7
|
+
except ImportError:
|
|
8
|
+
mcp = None
|
|
9
|
+
|
|
10
|
+
ROOT = os.path.join(os.path.dirname(__file__), "..")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@unittest.skipIf(mcp is None, 'pip install "mcp>=1.2,<2" to run this test')
|
|
14
|
+
class T(unittest.TestCase):
|
|
15
|
+
@classmethod
|
|
16
|
+
def setUpClass(cls):
|
|
17
|
+
cls.srv = ThreadingHTTPServer(("127.0.0.1", 0), H)
|
|
18
|
+
threading.Thread(target=cls.srv.serve_forever, daemon=True).start()
|
|
19
|
+
cls.b = f"http://127.0.0.1:{cls.srv.server_port}"
|
|
20
|
+
@classmethod
|
|
21
|
+
def tearDownClass(cls): cls.srv.shutdown(); cls.srv.server_close()
|
|
22
|
+
|
|
23
|
+
def test_stdio_roundtrip(self):
|
|
24
|
+
from mcp import ClientSession, StdioServerParameters
|
|
25
|
+
from mcp.client.stdio import stdio_client
|
|
26
|
+
import json
|
|
27
|
+
|
|
28
|
+
async def go():
|
|
29
|
+
params = StdioServerParameters(command=sys.executable, args=[os.path.join(ROOT, "link_check_lite_mcp.py")], cwd=ROOT)
|
|
30
|
+
async with stdio_client(params) as (r, w):
|
|
31
|
+
async with ClientSession(r, w) as s:
|
|
32
|
+
await s.initialize()
|
|
33
|
+
tools = await s.list_tools()
|
|
34
|
+
self.assertEqual([t.name for t in tools.tools], ["check_links"])
|
|
35
|
+
res = await s.call_tool("check_links", {"urls": [self.b + "/a", self.b + "/gone"]})
|
|
36
|
+
self.assertFalse(res.isError)
|
|
37
|
+
data = res.structuredContent["result"] if res.structuredContent else [json.loads(c.text) for c in res.content]
|
|
38
|
+
return data
|
|
39
|
+
data = asyncio.run(go())
|
|
40
|
+
self.assertEqual(data[0]["final_url"], self.b + "/ok")
|
|
41
|
+
self.assertEqual(data[1]["status"], 404)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
unittest.main()
|