check_links
Validate all URLs in a markdown document by issuing HEAD requests (falling back to GET). Detects broken links (4xx/5xx), redirects (3xx), and timeouts. Returns detailed report with broken URLs, redirect targets, and timed-out links.
Instructions
Validate every URL in a markdown document by issuing HEAD (then GET fallback) requests. FREE. Concurrency-limited to 10 in-flight requests; per-URL timeout 8s. Reports broken (4xx/5xx), redirected (3xx), and timed-out links. Returns: { total, ok, broken: [{ url, status }], redirects: [{ url, to }], timeouts: string[] }. Common errors: malformed markdown produces zero results without throwing.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| content | Yes | The markdown content containing URLs to check |
Implementation Reference
- src/tools/link-tools.ts:15-21 (handler)Handler function for the check_links tool. Validates input, calls checkLinks from audit/links.ts, and returns success/error response.
export async function handleCheckLinks(input: z.infer<typeof checkLinksSchema>) { const contentErr = validateRequired(input.content, "content"); if (contentErr) return makeError("VALIDATION_ERROR", contentErr); const result = await checkLinks(input.content); return makeSuccess(result); } - src/tools/link-tools.ts:7-9 (schema)Zod schema for check_links input: expects a single 'content' string field (markdown content containing URLs to check).
export const checkLinksSchema = z.object({ content: z.string().describe("The markdown content containing URLs to check"), }); - src/index.ts:234-238 (registration)Registration of the 'check_links' tool on the MCP server with description, schema, and handler invocation.
server.tool("check_links", "Validate every URL in a markdown document by issuing HEAD (then GET fallback) requests. FREE. Concurrency-limited to 10 in-flight requests; per-URL timeout 8s. Reports broken (4xx/5xx), redirected (3xx), and timed-out links. Returns: { total, ok, broken: [{ url, status }], redirects: [{ url, to }], timeouts: string[] }. Common errors: malformed markdown produces zero results without throwing.", checkLinksSchema.shape, async (input) => { const parsed = checkLinksSchema.parse(input); const result = await handleCheckLinks(parsed); return { content: [{ type: "text", text: formatToolResponse("check_links", result, formatLinkCheck) }] }; }); - src/audit/links.ts:125-151 (helper)Core link-checking function: extracts URLs from markdown, checks them in batches of 5, returns aggregated LinkCheckResult.
export async function checkLinks(markdown: string): Promise<LinkCheckResult> { const urls = extractUrls(markdown); if (urls.length === 0) { return { total: 0, ok: 0, redirected: 0, broken: 0, timeout: 0, error: 0, links: [] }; } // Check in batches of 5 to avoid overwhelming servers const results: LinkResult[] = []; const batchSize = 5; for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map((url) => checkSingleLink(url))); results.push(...batchResults); } return { total: results.length, ok: results.filter((r) => r.status === "ok").length, redirected: results.filter((r) => r.status === "redirected").length, broken: results.filter((r) => r.status === "broken").length, timeout: results.filter((r) => r.status === "timeout").length, error: results.filter((r) => r.status === "error").length, links: results, }; } - src/audit/links.ts:67-119 (helper)Helper that checks a single URL via HEAD request (with GET fallback on 405), handles timeouts and errors.
export async function checkSingleLink(url: string): Promise<LinkResult> { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), LINK_TIMEOUT_MS); try { let res = await fetch(url, { method: "HEAD", signal: controller.signal, redirect: "manual", headers: { "User-Agent": "Pipepost-LinkChecker/1.0", }, }); // Some servers reject HEAD — retry with GET if (res.status === 405) { clearTimeout(timeout); const controller2 = new AbortController(); const timeout2 = setTimeout(() => controller2.abort(), LINK_TIMEOUT_MS); try { res = await fetch(url, { method: "GET", signal: controller2.signal, redirect: "manual", headers: { "User-Agent": "Pipepost-LinkChecker/1.0", }, }); } finally { clearTimeout(timeout2); } } const code = res.status; if (code >= 200 && code < 300) { return { url, status: "ok", status_code: code, message: "OK" }; } if (code >= 300 && code < 400) { const location = res.headers.get("location") ?? "unknown"; return { url, status: "redirected", status_code: code, message: `Redirects to ${location}` }; } return { url, status: "broken", status_code: code, message: `HTTP ${code}` }; } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { return { url, status: "timeout", status_code: null, message: "Request timed out (5s)" }; } const message = err instanceof Error ? err.message : "Unknown error"; return { url, status: "error", status_code: null, message }; } finally { clearTimeout(timeout); } }