readability-mcp
The readability-mcp server transforms already-rendered (post-JavaScript) HTML into clean, LLM-friendly Markdown and metadata without making any outbound network requests. It provides three tools:
extract: Feeds full rendered HTML through Mozilla Readability to isolate the main article, then converts it to Markdown, HTML, plain text, or JSON. Supports:Extraction aggressiveness modes:
balanced,aggressive, orconservativeCSS selectors to include specific subtrees (e.g.
"main") or exclude boilerplate (navbars, footers)Markdown options: GFM, ATX vs. setext headings, fenced vs. indented code blocks, image handling (
keep,drop,src-only,reference)Optional YAML/JSON frontmatter metadata (title, author, date, etc.) resolved via JSON-LD → OpenGraph → Twitter →
<meta>cascadeHTML sanitization via DOMPurify (removes scripts, iframes)
Safe truncation at block boundaries (
maxChars)Relative link absolutization via an optional
urlparameter (no fetching)Structured diagnostics: readability score, fallback usage, removed node counts, truncation status
html_to_markdown: Converts arbitrary HTML snippets directly to Markdown without Readability's article-scoring step — ideal for pre-isolated content. Shares most options withextract.outline: Quickly extracts the heading hierarchy (h1–h6) with anchor IDs as a lightweight pre-check before committing to full extraction.
Click on "Install 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., "@readability-mcpExtract article from raw HTML and format as markdown with metadata"
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.
readability-mcp
Turn already-rendered HTML (captured post-JavaScript from a browser or chrome-devtools MCP) into clean, LLM-friendly Markdown + metadata, using Mozilla Readability, Turndown, and DOMPurify.
The key idea: rendering and extraction are decoupled. A real browser (chrome-devtools) owns rendering; this server only transforms HTML it reads from a file. The server makes no outbound requests — there is no fetch, no SSRF surface. Every HTML-input tool takes a localPath (a file on disk), never an inline string, so a full rendered page never enters the model context. The optional baseUrl is origin context only, used to absolutize relative links; it is never fetched.
Install
npm install readability-mcp
# or run on demand:
npx readability-mcpRequires Node >= 22. Build from source:
git clone <repo> && cd readability-mcp
yarn install
yarn build # bundles to dist/index.js
node dist/index.js # starts the stdio MCP serverDocker / Smithery
A Dockerfile (multi-stage node:22-bookworm-slim, runs as non-root node) and a smithery.yaml (stdio runtime) are included for container and Smithery deployment:
docker build -t readability-mcp .
docker run --rm -i readability-mcp # stdio MCP server on stdin/stdout
docker run --rm -i readability-mcp extract --format md < page.htmlThe Smithery manifest pins the stdio startCommand (this server ships StdioServerTransport only — the HTTP container runtime cannot launch it) and surfaces READABILITY_MCP_LOG_LEVEL as the one config knob.
Related MCP server: urltomarkdown-mcp
The chrome-devtools handoff
The motivating flow is two hops — each tool does the one thing it is best at:
// 1. In the chrome-devtools MCP, grab the RENDERED document (post-JS) and write
// it to a file via evaluate_script's `filePath` arg (the model emits only a
// path, never the page bytes):
mcp__chrome-devtools__evaluate_script({
function: () => document.documentElement.outerHTML,
filePath: "/tmp/page.html",
});
// 2. Point readability-mcp at that file.
// `baseUrl` is OPTIONAL context (origin for absolutizing relative links) — never fetched.
mcp__readability__extract({ localPath: "/tmp/page.html", baseUrl: pageUrl });This matters most for SPAs and JS-augmented pages, where the initial HTML is an empty <div id="root"> and only the post-JS DOM has the content.
MCP client config
Add to your MCP client config (Claude Code, Claude Desktop, etc.):
{
"mcpServers": {
"readability": {
"command": "npx",
"args": ["-y", "readability-mcp"]
}
}
}Tools
All eleven always-on tools return MCP structured content (schemaVersion plus a tool-specific payload of metadata / diagnostics / items / …) validated by a zod outputSchema, plus a human/LLM-readable payload in content[0].text. A sampling-capable host also sees a twelfth — summarize — registered after the initialize handshake when the client advertises the MCP sampling capability. Nothing throws across the wire — failures become { "isError": true } results. Every input and output field carries a description in the tool's JSON schema, so clients can introspect each option without reading these docs.
localPath — the only HTML input. Every HTML-input tool takes a localPath pointing at a file holding the already-rendered (post-JavaScript) HTML. The server reads the bytes itself, so the page never enters the model context — the model emits only a path string. The motivating hop is chrome-devtools → readability: evaluate_script writes document.documentElement.outerHTML to a file via its filePath arg, then the tool reads that path. Resolved relative to the server process working directory; prefer absolute paths so the chrome-devtools capture and this read agree on location.
extract — primary tool
Extracts the main article from rendered HTML and returns Markdown + metadata + diagnostics.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; used to absolutize relative links/images. |
|
|
|
|
|
|
|
|
|
| — | Restrict extraction to a subtree: |
| — | Strip boilerplate before Readability: |
| — | Perf/safety cap = Readability |
| — | Semantic alias for Readability |
|
| Tables, strikethrough, task lists. |
|
|
|
|
|
|
|
|
|
| — |
|
|
| Run DOMPurify on the article HTML. |
| — | Truncate the payload at a block boundary — never inside a fenced code block. |
|
| For |
|
| Retain all classes (default strips non-language classes). |
| — | Escape hatch — passed verbatim to |
| — | Split the extracted markdown into token-bounded chunks (RAG/embedding-ready). |
|
| Emit |
|
| Emit |
Fallback. If Readability's parse() returns no article (e.g. an app shell or image-only page), a selector cascade salvages the first usable root — article → main → [role=main] → largest text-dense block → body — and reports diagnostics.fallbackUsed: true with extractedNode naming the root that was used.
Metadata cascade. Each metadata field is resolved by priority: JSON-LD → OpenGraph → Twitter → <meta>/<time> → Readability → <title> (first non-empty value wins). When the page carries schema.org JSON-LD, metadata.structured exposes the parsed primary object (Recipe/Product/Event/HowTo/Article…) with @context stripped and @type normalized, so non-article content rides on extract without a separate tool. Alongside the bibliographic fields, metadata carries wordCount, readingTimeMin, and tokenEstimate (with estimator: "chars/4" naming the heuristic) — an advisory count for context budgeting; the host re-counts before sending, so a model-specific tokenizer isn't worth the weight.
html_to_markdown — fragment path
Converts an arbitrary HTML fragment to Markdown without Readability scoring (e.g. a snippet already isolated via chrome-devtools). Same Turndown + DOMPurify path; reports fallbackUsed: true, extractedNode: "fragment". Takes localPath plus the same format, gfm, headingStyle, codeBlockStyle, images, tables, sanitize, maxChars, wordsPerMinute, selectors, baseUrl, and debug options as extract. Metadata is minimal (baseUrl, wordCount, readingTimeMin, and a title from the fragment's first heading).
extract_section — one section by selector or heading
Returns just one section of a document — "give me the Authentication section" on a long doc without paying for full extraction. A thin resolver over extract's selectors.include path, not a new extractor: selector mode passes straight through, and heading mode wraps the matched subtree in <section data-rdrm-section-scope> before routing through the same selectors.include path.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; used to absolutize relative links/images. |
| — | CSS selector scoping extraction to one subtree; passed straight through as |
| — | Heading text selecting one section; the section spans from this heading to the next same-or-higher-level heading. Case-insensitive; first exact match wins, falling back to the first substring contain. Provide exactly one of |
Output shape is the same as extract (content, metadata, diagnostics). Heading mode is equivalent to selector mode on the same subtree: heading: "Authentication" discovers the same boundary a wrapping <section id="auth">…</section> would expose via selector: "#auth". A non-matching heading yields { "isError": true } with no heading matched: <query>.
extract_tables — every table on the page
Extracts every <table> on the page — a querySelectorAll('table') walk (page-wide by default; narrow it with selectors.include) in front of the same rowspan/colspan-aware matrix serializer used by the tables option on extract. Runs no Readability, Turndown, sanitization, or normalizeDocument chrome-stripping, so it captures tables outside the article body (nav, aside, footer, boilerplate) that the tables option on extract never sees — the motivating case is wiki/doc/data pages whose content is table-heavy but whose article boundary hides most of them.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; carried through to |
|
|
|
| — | Same |
Output shape: structuredContent.tables = [{index, rows, cols, markdown}] — one entry per non-empty table in document order, where rows/cols are the matrix dimensions after rowspan/colspan resolution and markdown is the table rendered in the requested format. All entries' markdown are joined by blank lines into content[0].text ("(no tables found)" when the page has none). metadata = {baseUrl?, format, tableCount}. Empty <table> elements (no rows) are skipped, so index is contiguous over the emitted tables. Nested <table>s are emitted as their own entries in document order (the matrix walk excludes nested tables from a parent's matrix; querySelectorAll then returns the nested table separately).
extract_grid — CSS-grid / div tables (the div equivalent of extract_tables)
Detects and extracts a CSS-grid / div "table" — for SPA pages that render data into repeating <div> rows instead of <table> (analyst-estimate tables, financial summaries, comparison grids) where extract_tables returns nothing. Two modes: auto-detect finds the container whose direct children form the largest same-shape sibling group of ≥3 rows (each row a set of ≥2 direct element-children, outside nav/header/footer/aside); selector mode takes explicit rowSelector + cellSelector (cells scoped to each row subtree). The detected matrix is rendered through the same gfm/csv/json matrix renderer as extract_tables. Runs no Readability, Turndown, sanitization, or normalizeDocument chrome-stripping in selector mode (auto mode strips chrome internally so an article's nav doesn't look like a 4-row grid).
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; carried through to |
|
|
|
| — | Same |
| — | CSS selector for repeating row containers. When set with |
| — | CSS selector for cells within each row (scoped to the row subtree). Required together with |
Output shape: structuredContent = {schemaVersion, content, grid, diagnostics, metadata}. grid = {rows, cols, markdown} — the single detected grid (rows/cols 0 and markdown empty when nothing is detected), where markdown is the grid rendered in the requested format (ragged rows are padded to a dense rectangular matrix). content[0].text is the grid markdown, or "(no repeating grid found)". diagnostics = {detected, rowCount, colCount, containerSelector, rowTag, confidence, note}: detected:false means no grid structure was found; containerSelector/rowTag name the winning cluster (or rowSelector in selector mode); rowCount counts emitted rows (a recovered header row included); confidence is high when ≥6 detected data rows, medium when ≥3, low otherwise (a recovered header is inference and does not raise it). metadata = {baseUrl?, format, detected}.
extract_list — feed/index/search pages
A second engine for pages Readability cannot turn into one article: HN-style feeds, search-result pages, blog indexes, product grids. Strips nav/header/footer/aside + ARIA chrome roles first (the false-positive guard so an article's nav menu doesn't look like a 4-item feed), then finds the container whose direct children form a same-shape sibling cluster of ≥3 elements each carrying a navigation anchor — the cluster with the most items wins. Runs no Readability, Turndown, sanitization, or normalizeDocument chrome-stripping (the detector scores against the very chrome-bearing structure the article normalizer would discard). Returns detected:false on article pages.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; used to absolutize item |
| — | Same |
Output shape: structuredContent = {schemaVersion, content, items, diagnostics, metadata}. items = [{title, url, snippet, score}] in document order — snippet is the item's text teaser (empty when the cluster has no per-item text), score is the detector's internal ranking weight. diagnostics = {detected, itemCount, containerSelector, itemTag, confidence, note}: detected:false means no list structure was found (itemCount:0, empty items, and a note explaining why); containerSelector/itemTag name the winning cluster; confidence is a rough quality signal. metadata = {baseUrl}.
outline — heading pre-check
Returns the document outline (h1–h6 in document order with stable anchor ids) as a cheap "is this worth reading?" / "where's the section about X?" pre-check before paying for full extraction. Runs no Readability, Turndown, or sanitization — a pure heading walk over the normalized DOM.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; carried through to |
| — | Same |
Output shape: structuredContent.outline = [{level, text, anchor}] plus an indented-bullet TOC rendered into content[0].text, and metadata = {title?, baseUrl?} (title falls back from <title> to the first <h1>). Anchor precedence: the heading's own id, then a descendant permalink's #fragment, then a slug of the text (deduped -1, -2, … for generated slugs only — author ids are kept verbatim).
extract_links — anchor inventory for crawl/navigation
Returns a structured list of anchor links — [{text, href, rel, isExternal}] in document order — gathered from the raw parsed DOM. Runs no Readability, Turndown, sanitization, or normalizeDocument chrome-stripping, so nav/footer/main links survive (the crawl-relevant ones). Pairs with chrome-devtools for crawl/navigation decisions: the host picks the next page without re-parsing HTML.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; absolutizes relative |
|
| Drop cross-origin links; keep same-origin, relative, fragment, and non-http(s) ( |
| — | Same |
Output shape: structuredContent.links = [{text, href, rel, isExternal}] plus a - [text](href) rendering in content[0].text. href is absolutized against baseUrl (unchanged when baseUrl is absent or the pair fails to parse). isExternal is true only when baseUrl is provided and the absolutized href parses to a different HTTP(S) origin — relative, fragment, same-origin, mailto:/tel:/javascript:, and malformed hrefs are all false. rel is the raw attribute value ("noopener noreferrer", "nofollow", …) or "" when absent. Anchors with no href are skipped; the rest are kept in document order with no deduplication.
extract_metadata — bibliographic pre-check
Returns only the bibliographic metadata — title, byline, siteName, lang, publishedTime, excerpt, canonical, baseUrl — without running Readability/Turndown, as a fast pre-check for crawlers and citation. Short-circuits the pipeline before the article body is scored; resolves the same metadata cascade as extract (JSON-LD → OpenGraph → Twitter → <meta>/<time> → <title>), plus <link rel="canonical"> → og:url for canonical. The baseUrl field is the origin you passed in; canonical is the page's declared canonical — they often differ.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; carried through to |
Output shape: structuredContent.metadata = {title?, byline?, siteName?, lang?, publishedTime?, excerpt?, canonical?, baseUrl?} plus a human-readable key: value rendering in content[0].text. Note: wordCount/readingTimeMin/tokenEstimate are not populated by this tool — they are meaningless without the extracted body.
explain — extraction post-mortem
Post-mortem diagnostics for an extract call: surfaces why Readability picked what it picked. Runs the same normalize + Readability pipeline as extract (no fallback cascade, no Turndown, no DOMPurify) and reads Readability's real per-candidate contentScore values off the DOM expando Readability stamps during scoring. Reach for it when extract lands on the wrong root or strips content you expected — it shows the scored runners-up so you can tune selectors/extraction/minArticleLength.
Option | Default | Description |
| — | Path to a file holding the rendered HTML (post-JS), e.g. |
| — | Optional origin. Never fetched; used for pagination/gating detection only. |
| — | Same |
|
| Maximum scored candidate nodes to return (highest first); 1–20. |
Output shape: structuredContent = {schemaVersion, content, chosenRoot, candidates, readerable, parseSucceeded, fallbackUsed, gating, pagination, removedNodes, snapshot}. chosenRoot is Readability's raw top pick (before parent-walking/only-child post-processing); candidates is the ranked list (capped at topN) where each entry carries {tag, id, className, selector, score, textLength} — score is Readability's actual contentScore, not a self-rolled heuristic, and selector is a CSS-ish hint, not a unique locator (the score lives on a JS expando invisible to CSS). removedNodes = {total, chrome, boilerplate}. gating/pagination mirror extract's diagnostics (null when none). snapshot = {html, truncated} is the post-normalize, pre-Readability HTML — "what Readability saw" — capped at 4000 chars. parseSucceeded:false is the signal that extract would have hit its fallback cascade; fallbackUsed is always false here (explain never runs the cascade).
chunk_text — chunk for RAG/embedding
Splits already-extracted text into token-bounded chunks, each carrying index, text, tokenCount (chars/4, same estimator as metadata.tokenEstimate), and headingContext (the heading hierarchy path in effect at the chunk's first unit — empty string when the chunk precedes any heading). Operates on any text — pair with extract's chunk option when you want chunks inline with the extraction.
Option | Default | Description |
| — | Already-extracted text to split (e.g. markdown from |
|
| Per-chunk token budget. No chunk exceeds this; oversized blocks are split by line, then hard-split. |
|
| Tokens to overlap between consecutive chunks ( |
|
| Chunking strategy. |
Output shape: structuredContent.chunks = [{index, text, tokenCount, headingContext}] in order, plus a readable numbered index in content[0].text. Empty array when the input has no non-whitespace content.
summarize — host-model summarization (sampling-gated)
Delegates summarization to the host's model via MCP sampling/createMessage — the server embeds no model and calls no provider directly. Only listed when the connected client advertises the sampling capability on initialize (registered after the handshake, so a non-sampling host never sees it on tools/list); otherwise invisible. The host picks the model and may prompt the user before each call (human-in-the-loop, per MCP). Hand it the output of extract/extract_section/html_to_markdown/chunk_text — or any markdown/text string.
Option | Default | Description |
| — | Markdown or text to summarize. Passed through to the host model verbatim; the server does not parse or modify it. |
|
| Upper bound on the summary length in tokens, forwarded as |
Output shape: a single content[0].text entry holding the host's summary. No structuredContent — the server returns whatever the host model produces. A non-text response from the host (e.g. an image) surfaces as { "isError": true }.
Diagnostics
structuredContent.diagnostics exposes: readerable, extractedNode, fallbackUsed, removedNodes (element delta vs. the document), chromeRemoved and imagesResolved (pre-conversion cleanup counts), boilerplateRemoved (related-posts / newsletter-signup / read-next blocks stripped before conversion, footprint-guarded so article content is never deleted), sanitization.{scripts,iframes} (counted across the whole pipeline), pagination ({type:"paginated"|"infinite", nextUrl?, selector?} — detection only; the host drives loading, this server never fetches), gated ({likely, reason} — detection only; signals a likely paywall/metered gate so the host knows the extraction may be partial — this server never fetches or authenticates), truncated, and trace (per-stage {stage, ms} timings — debug-only, emitted only when debug:true is passed to extract/html_to_markdown; absent otherwise). Stages are non-overlapping and ordered: normalize, readability, sanitize, turndown, metadata on the article path (html_to_markdown omits readability); on the fallback path a single fallback stage covers sanitize + turndown so the timings still sum to the pipeline's wall-clock.
Rich content
Code-block language tags. Before Readability scores the document, real-world code-block conventions are canonicalized to
<pre><code class="language-X">so the language survives Readability's class stripping and Turndown emits a tagged fence. Mapped conventions: GitHub<div class="highlight highlight-source-js">wrappers (-shell,-python, …), React/sandpack<pre class="sp-javascript">, and genericlang-X/brush: X. Common language tokens are added to Readability'sclassesToPreserveso```js/```shellland in the markdown instead of a bare fence; exotic languages fall back to an untagged fence. Automatic (no option);html_to_markdownis unaffected (it skips Readability).Footnotes. When an article pairs
<sup>reference markers with a definitions list (<ol class="footnotes">,<ol class="references">,[role="doc-endnotes"], or standalone<li id="fn-…">/<li id="cite_note-…">), both halves are auto-converted to Markdown footnote syntax — inline[^N]markers in place of the<sup>and an appended[^N]: definitionblock. The conversion is automatic (no option); when no footnote markup is detected, output is byte-identical to a plain turndown.Math. KaTeX (
<span class="katex">with an<annotation encoding="application/x-tex">) and MathJax (<script type="math/tex">/mode=display) are auto-converted to$…$(inline) or$$…$$(display) LaTeX before turndown runs, so raw backslashes survive unescaped and the rendered spans never leak. The conversion is automatic (no option); when the source LaTeX is absent (a broken.katexwith no annotation, an empty MathJax script), a[?]placeholder is emitted in its place — never a crash.
Payload size (stdio)
A full rendered SPA can be several MB as a string, and MCP tool args travel over JSON-RPC on stdio. Mitigations:
Scoped capture (recommended) — real pages are large (hundreds of KB of
outerHTML), so capture only what you need rather than the whole document. Via chrome-devtoolsevaluate_script(with itsfilePatharg), writedocument.head.outerHTML(for metadata) plus a content subtree such asdocument.querySelector('article')?.outerHTML || document.querySelector('main')?.outerHTMLto a file, then pointextractat it vialocalPath.baseUrlabsolutizes relative links within whatever HTML is passed.selectors.include— scope to the article subtree, e.g."main", so only the relevant DOM is scored and serialized.maxChars— cap the returned payload; truncation lands at a block boundary and never splits a fenced code block.maxNodes— a hard cap on elements parsed (Readability.maxElemsToParse) for very large documents.
Prompts
The server exposes one MCP prompt:
prompts/read_url({url})— returns the recipe that choreographs the canonical two-tool flow for reading a live URL: a browser tool (chrome-devtools) renders the page, writesdocument.documentElement.outerHTMLto a file viaevaluate_script'sfilePatharg, then the readabilityextracttool reads that file vialocalPathand turns it into Markdown. The prompt's job is to fill the host in on the handoff (the readability server never fetches URLs); the host executes the steps. Theurlargument is carried into the recipe asbaseUrl(origin context) forextract.
Resources (page cache)
extract({cache:true}) caches the result and exposes it as an addressable MCP Resource at readability://page/{hash}. Subsequent extract calls with the same HTML (modulo volatile bytes — see below) and the same output options hit the cache instead of re-running the pipeline. The cache is in-memory, bounded (256 entries, LRU), and TTL'd (30 min).
diagnostics.cache = {hit, normalizedHash, originalHash}appears on every cachedextractresult:hit:true/false, plus both hashes. ThenormalizedHashis what the key is built from;originalHashis the SHA-256 of the raw HTML. A miss wherenormalizedHashmatches an existing entry but the lookup still missed points at an args-fingerprint mismatch (differentformat/selectors/…) rather than a genuinely different page — useful when debugging "should have hit."Normalized-hash keying. Before hashing, the HTML is volatility-normalized: inline
<script>blocks, CSP<meta>tags, per-rendernonce=attributes, build-tool generated attribute names (data-v-…,data-css-…,data-svelte-…,data-h-…), and React/Next generatedids (:R1:,:r1:,__next_…,reactX_…) are stripped, and whitespace runs are collapsed. The same page re-rendered with a fresh CSP nonce or a different build hash collapses to the same key.Listing and reading.
resources/listenumerates current cache entries (readability://page/{cacheKey},text/markdown);resources/readon areadability://page/{hash}URI returns the cached markdown (empty body if the entry has expired or been evicted).
CLI
readability-mcp also runs as a one-shot CLI for extracting from a local HTML file or stdin, with no MCP server in the loop:
readability-mcp extract [file.html] [--format md|json|html] [--max-chars N]extractis the only subcommand; everything after it is parsed as options. With no args at all (readability-mcp), the stdio MCP server starts instead.file.htmlis read from disk; when no file is given, HTML is read from stdin.--format:md(default, markdown) |json(thestructuredContentobject, pretty-printed) |html(the post-pipeline HTML). Internallyjsonreuses the markdown pipeline and serializes the structured object on the way out.--max-chars Nmirrorsextract'smaxChars— truncate the payload at a block boundary, never inside a fenced code block.
curl -s https://example.com | readability-mcp extract --format md
readability-mcp extract page.html --format json --max-chars 20000
cat saved.html | readability-mcp extractDevelopment
yarn typecheck # tsc --noEmit
yarn build # vite build -> dist/index.js
yarn lint # eslint
yarn test # vitest run
yarn test:update-goldens # UPDATE_GOLDENS=1 vitest runBenchmark
yarn bench prints a per-fixture metrics table (input nodes, markdown chars, token estimate, compression ratio, removed nodes, and preserved images/tables/links) plus a unified content delta against committed baselines under test/bench/baseline/. It also prints a precision/recall table of the extracted main content vs human-labeled boundaries (test/bench/labels.ts — one CSS selector per fixture naming its article container), with a macro-average aggregate row, and an aggregate per-stage timing breakdown averaged from the debug trace across fixtures. The bench runs in CI as a non-blocking job (continue-on-error: true), so a regression is surfaced, not gating; bench.test.ts additionally fails yarn test if the committed metrics or scores drift out of sync.
yarn bench # print metrics + content deltas + PR/timing tables
BENCH_UPDATE=1 yarn bench # refresh baselines (do deliberately, like UPDATE_GOLDENS)Per-fixture fields: inputNodes (parsed element count), markdownChars/tokens (output size, chars/4), compressionRatio (output chars per input node), removedNodes (element delta across the pipeline), and images/tables/links (preserved content counts). PR fields: precision (fraction of extracted word tokens inside the labeled main content), recall (fraction of labeled tokens recovered), f1 (harmonic mean), extractedTokens/labeledTokens (multiset sizes). Fixtures with no prose (the image-only fallback gallery) score N/A and are excluded from the aggregate.
License
MIT
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Tools
Related MCP Servers
- AlicenseAqualityDmaintenanceConverts web pages and HTML strings into clean, LLM-optimized Markdown with metadata extraction and token estimation. It uses a lightweight, browserless approach to provide token-efficient output for more effective LLM processing.2MIT
- AlicenseAqualityDmaintenanceConverts URLs and raw HTML to clean Markdown, enabling AI assistants to read web pages for summarization, analysis, or ingestion.261MIT
- Flicense-qualityCmaintenanceConverts raw HTML to clean Markdown, optimized for LLM token efficiency and deterministic processing.93
- Alicense-qualityDmaintenanceConverts any webpage into clean, LLM-ready Markdown, removing noise and supporting JavaScript rendering.MIT
Related MCP Connectors
Converts any URL to clean, LLM-ready Markdown using real Chrome browsers
Read any web page as clean Markdown for AI agents: fetch, search, metadata, links. SSRF-safe.
Web scraping for AI agents. Converts URLs to clean, LLM-ready Markdown with anti-bot bypass.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/v1nvn/readability-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server