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.
@@ -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,3 @@
1
+ include CHANGELOG.md
2
+ include llms.txt
3
+ include server.json
@@ -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
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/tidybytetools/link-check-lite/blob/main/LICENSE)
34
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
35
+ [![MCP server](https://img.shields.io/badge/MCP-stdio-purple.svg)](#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
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/tidybytetools/link-check-lite/blob/main/LICENSE)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
5
+ [![MCP server](https://img.shields.io/badge/MCP-stdio-purple.svg)](#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
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/tidybytetools/link-check-lite/blob/main/LICENSE)
34
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/)
35
+ [![MCP server](https://img.shields.io/badge/MCP-stdio-purple.svg)](#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,3 @@
1
+ [console_scripts]
2
+ link-check-lite = link_check_lite:main
3
+ link-check-lite-mcp = link_check_lite_mcp:main
@@ -0,0 +1 @@
1
+ mcp<2,>=1.2
@@ -0,0 +1,2 @@
1
+ link_check_lite
2
+ link_check_lite_mcp
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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()