linkcheck
Checks URLs against the URLhaus threat feed from abuse.ch to identify malicious destinations and provide threat intelligence as part of link safety verdicts.
Scans agent skills from GitHub URLs, flags risky patterns in the files, pins GitHub sources to a specific commit, and checks every link contained in the skill.
Optional owner-only integration that sends owner-checked links to VirusTotal for additional threat reputation and malware analysis when a self-hosted instance is configured with a VirusTotal API key.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@linkcheckis this link safe to open? https://login-paypa1-secure.com/verify"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Give linkcheck a link and it:
Follows the redirects reading only response headers (HEAD first; GET with the body discarded if a server refuses HEAD), so the page never loads and nothing runs.
Strips trackers like
utm_*andfbclid, and lists what it removed.Inspects the shape for phishing tricks: lookalike characters,
@tricks, raw IP hosts, abused domain endings.Checks the destination against URLhaus and five HaGeZi lists (threat intel, newly registered domains, shorteners, abused domain endings), refreshed daily.
Then it returns SAFE, SUSPICIOUS or DANGEROUS with the evidence. If any check can't finish, the verdict can only get stricter. It never falls back to SAFE.
Verdicts are a best-effort signal, not a guarantee. A SAFE link can still turn out to be malicious, and a SUSPICIOUS one can be fine. Use it as one input, not the final word.
Privacy: linkcheck doesn't save the links you check. To check one, it looks up the hostname with Cloudflare's DNS service and requests headers from the link's servers, with a user agent that names this project. Self-hosted instances with a VirusTotal key also send owner-checked links to VirusTotal.
Known limits:
Checking a link means requesting it. A few sites treat any request to a one-click link (an unsubscribe link, a magic sign-in link) as a click, so check those with care.
Only redirects that come back as HTTP headers are followed. Redirects done with JavaScript or a
<meta refresh>inside the page are not seen, because the page is never loaded.
Connect in 30 seconds
The public instance is free and needs no account or key.
Claude: Settings → Connectors → Add custom connector. Paste the URL below and pick No sign-in.
https://linkcheck-mcp.kazani.workers.dev/mcpOther MCP clients:
{
"mcpServers": {
"linkcheck": { "type": "http", "url": "https://linkcheck-mcp.kazani.workers.dev/mcp" }
}
}Public checks use the threat feeds and structural checks. VirusTotal is reserved for the maintainer's own key; self-host with your own key to get it.
Related MCP server: unphurl-mcp
Tools
check_url(url, compact?): one link in, a verdict out, with the redirect chain, removed trackers, and every feed hit.compact: truereturns just{verdict, verdictReason, resolvedUrl, degraded}.check_text(text, compact?): finds every link in an email, message or page (including defanged ones likehxxp://evil[.]com), checks each, and returns aworstVerdict. It also surfaces text hidden from a human reader: Unicode tag text, zero-width characters, sentences inidattributes and base64 text as strong signals, and HTML comments and hidden elements as weak ones.check_skill(files | github_url): scans an agent skill before you install it. It flags risky patterns in the files, pins GitHub sources to a commit, and checks every link inside. The best verdict isNO_FLAGS, neverSAFE.
Inspired by the Android app URLCheck, reshaped for an AI agent instead of a human tap.
Endpoints
GET /— human landing page (served fortext/htmlrequests; never shadows the MCP transport on the same origin).GET /health— per-feedlast_status/last_refresh.GET /.well-known/mcp/server-card.json— machine-readable server descriptor for MCP discovery.POST /mcp— the MCP transport.POST /check— plain-HTTP verdict ({url, compact?}) for non-MCP clients, e.g. a shell pre-flight hook that checks links before an agent acts on them. Same logic ascheck_url.
Stack
Cloudflare Worker · stateless createMcpHandler (MCP SDK v2) · D1 (small feeds) + KV (large feeds + verdict cache) · daily Cron Trigger for feed refresh.
Feed refresh
Threat feeds refresh daily via a Cloudflare Cron Trigger (17 6 * * * UTC). Small feeds (URLhaus, HaGeZi Shortener/Abused-TLD, all in D1) are diffed — only added/removed rows are written, chunked to stay under D1's 100-bound-parameter-per-query limit. Large feeds (HaGeZi TIF-domains/TIF-IPs/Entropy-NRD, all in KV) are fully overwritten each refresh — one PUT regardless of list size. A failed source doesn't block the others; check /health for per-feed last_status/last_refresh.
To trigger a refresh manually (local wrangler dev can't fire a real cron tick on macOS < 13.5, so this is also how the refresh logic is verified against production):
curl -X POST https://<your-worker>.workers.dev/admin/refresh-feeds \
-H "authorization: Bearer <token>"Self-host
Local development / testing
wrangler dev does not work on macOS < 13.5 (workerd requirement) — verify locally with the real test suite instead, which drives the actual web-standard MCP handler in plain Node:
npm install
npm test # node --test 'src/**/*.test.ts'
npm run typecheck # tsc --noEmitDeploy
Create the D1 database and KV namespace (one-time), then wire their IDs into
wrangler.jsonc'sd1_databases/kv_namespaces:npx wrangler d1 create linkcheck-mcp-feeds npx wrangler kv namespace create linkcheck-mcp-cacheApply the schema and seed the feeds (see
migrations/*.sqlfor schema; feed data is fetched fresh from HaGeZi/URLhaus/ClearURLs upstream sources — see Sources below — and loaded viawrangler d1 execute --remote --file=...for the small feeds andwrangler kv key put --path=... --remotefor the large ones as single blobs).Secrets (optional):
npx wrangler secret put VIRUSTOTAL_API_KEYadds VirusTotal lookups.npx wrangler secret put OWNER_TOKENdecides who gets them. VirusTotal runs only for the owner: requests carryingAuthorization: Bearer <OWNER_TOKEN>(in Claude's Add custom connector dialog: No sign-in, then a request headerauthorization=Bearer <OWNER_TOKEN>). Everyone else gets the same checks without VT, and the full result shows"virusTotalChecked": false. WithoutOWNER_TOKEN, nobody gets VT and/admin/refresh-feedsis refused.npx wrangler secret put MCP_BEARER_TOKENlocks the whole server to one token, for a private instance. The public instance leaves it unset. The owner token still passes this lock, but the landing page Try-it box (which sends no token) stops working.
Deploy:
npx wrangler deployVerify:
curl https://<your-worker>.workers.dev/health curl -X POST https://<your-worker>.workers.dev/mcp \ -H "content-type: application/json" -H "accept: application/json, text/event-stream" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"check_url","arguments":{"url":"https://example.com"}}}'(Add
-H "authorization: Bearer <token>"if you setMCP_BEARER_TOKEN.)
Connect a client
Claude: Settings → Connectors → Add custom connector → paste the server URL (https://<your-worker>.workers.dev/mcp) and choose No sign-in. If you set a token, add it as a request header in the same dialog.
Any other MCP client, with a bearer token: via mcp-remote:
{
"mcpServers": {
"linkcheck": {
"command": "npx",
"args": ["mcp-remote", "https://<your-worker>.workers.dev/mcp", "--header", "Authorization: Bearer <token>"]
}
}
}Either way, install skills/check-url-safety.md to ~/.claude/skills/check-url-safety/SKILL.md — it teaches Claude to call these tools proactively on untrusted links, rather than waiting to be asked. (Do this after connecting — the skill references tools that don't exist until the connector is live.)
Sources and licenses
Code: MIT.
Tracker rules: a snapshot of the ClearURLs rules ships in
src/data/clearurls-rules.jsonunder its own license, LGPL-3.0 (seesrc/data/NOTICE.md).Threat intel, downloaded at runtime and not stored in this repo: HaGeZi dns-blocklists and HaGeZi nrd (GPL-3.0), and URLhaus under the abuse.ch terms.
Optional: VirusTotal, owner requests only.
Design inspiration: URLCheck (CC BY 4.0).
Built by kazani.
This server cannot be deployed
Maintenance
Related MCP Connectors
Scam and phishing detection for AI agents: safe/warn/danger verdicts for URLs and messages.
Threat intel for AI agents: IOCs, CVEs (EPSS/KEV), wallet sanctions and age, domain and URL checks.
Pay-per-call safety checks for AI agents: screen a crypto address or URL before you transact.
URL intelligence for AI agents and developers. 16 tools, 25 signal weights, 20 free checks.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceValidates URLs and checks their reputation to help identify AI hallucinations and verify web page authenticity.MIT
- AlicenseAqualityDmaintenanceURL intelligence for AI agents. One URL in, structured security and data quality signals out across 7 dimensions. 13 tools, risk score 0-100 with 23 configurable weights.1636 npm1MIT
- AlicenseAqualityAmaintenanceEnables AI agents to check URL safety before fetching content, using Google Web Risk, URLhaus, PhishTank, and AI analysis to return SAFE/SUSPICIOUS/DANGEROUS verdicts.1116 npm1MIT
- AlicenseNot gradedqualityFmaintenanceA URL security scanner and MCP server that enables AI agents to analyze URLs for phishing, malware, and other threats before navigation, with optional intent alignment checks.6Apache 2.0