Skip to main content
Glama
v1nvn

readability-mcp

by v1nvn

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-mcp

Requires 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 server

Docker / 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.html

The 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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; used to absolutize relative links/images.

format

markdown

markdown | html | text | json. json emits {metadata, content, diagnostics}.

metadataMode

none

none | yaml | json — prepend a metadata block to the markdown/text payload.

extraction

balanced

balanced | aggressive | conservative — maps to Readability's scorer knobs.

selectors.include

Restrict extraction to a subtree: "main", "article", ".post".

selectors.exclude

Strip boilerplate before Readability: ["nav", "footer", "[role=banner]"].

maxNodes

Perf/safety cap = Readability maxElemsToParse.

minArticleLength

Semantic alias for Readability charThreshold.

gfm

true

Tables, strikethrough, task lists.

headingStyle

atx

atx (#) | setext (underlining).

codeBlockStyle

fenced

fenced (```) | indented.

images

keep

keep | drop | src-only (bare URL) | reference (link-ref style).

tables

gfm (default, native) | csv | json — render <table> elements via a rowspan/colspan-aware matrix IR. csv/json emit fenced code blocks; gfm re-renders native tables so headerless and span-degenerate tables round-trip consistently. When unset, tables pass through Turndown's native rule.

sanitize

true

Run DOMPurify on the article HTML.

maxChars

Truncate the payload at a block boundary — never inside a fenced code block.

wordsPerMinute

200

For readingTimeMin.

keepClasses

false

Retain all classes (default strips non-language classes).

readabilityOverrides

Escape hatch — passed verbatim to new Readability(doc, …). Unstable.

chunk

Split the extracted markdown into token-bounded chunks (RAG/embedding-ready). {maxTokens, overlap?, strategy?} (strategy defaults to semantic) — when set, structuredContent.chunks is an array of {index, text, tokenCount, headingContext}. Only applies to format:"markdown" | "text"; HTML/JSON payloads carry no markdown body to slice and leave chunks unset.

imageInventory

false

Emit structuredContent.images — an array of {src, alt, width?, height?, caption} for every <img> in the extracted article (absolute resolved srcs, placeholders skipped, caption from the enclosing <figure>'s <figcaption> else alt). Independent of the images inline-rendering option.

debug

false

Emit diagnostics.trace with per-stage {stage, ms} timings (normalize, readability, sanitize, turndown, metadata). Debug-only — trace is absent otherwise.

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 — articlemain[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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; used to absolutize relative links/images.

selector

CSS selector scoping extraction to one subtree; passed straight through as selectors.include. Provide exactly one of selector/heading.

heading

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 selector/heading.

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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; carried through to metadata.baseUrl.

format

gfm

gfm (default, native GFM table with a delimiter row) | csv (RFC-4180-ish, quoted fields) | json (array of row objects keyed by the header row).

selectors

Same include/exclude shape as extract. Scope the walk: include:"#shareholding" returns only tables inside that subtree; exclude:[".ads"] drops matches anywhere on the page.

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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; carried through to metadata.baseUrl.

format

gfm

gfm (default, native GFM table with a delimiter row) | csv (RFC-4180-ish, quoted fields) | json (array of row objects keyed by the header row — first row used as keys when non-empty, else column_N).

selectors

Same include/exclude shape as extract. Scope auto-detection: include:"#estimates" narrows the scan to that subtree.

rowSelector

CSS selector for repeating row containers. When set with cellSelector, selector mode is used (no auto-detection). Example: 'div[class*="estimate-row"]'.

cellSelector

CSS selector for cells within each row (scoped to the row subtree). Required together with rowSelector — both-or-neither (setting only one is rejected). Example: 'div[class*="cell"]'.

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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; used to absolutize item hrefs.

selectors

Same include/exclude shape as extract. include picks which list the detector scores against — note the detector is comparative ("the cluster with the most items wins"), so pre-scoping to one container subverts that comparison; treat it as an "I know which list I want" escape hatch.

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 (h1h6 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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; carried through to metadata.baseUrl.

selectors

Same include/exclude shape as extract. include:"main" scopes the heading walk to that subtree, dropping nav/footer headings from the outline.

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).

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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; absolutizes relative hrefs and drives isExternal.

sameOriginOnly

false

Drop cross-origin links; keep same-origin, relative, fragment, and non-http(s) (mailto/tel/javascript) links.

selectors

Same include/exclude shape as extract. DOM-level scope (e.g. include:"#peers") applied before the link walk; composes with sameOriginOnly's semantic filter.

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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; carried through to metadata.baseUrl.

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

localPath (required)

Path to a file holding the rendered HTML (post-JS), e.g. document.documentElement.outerHTML written to disk by a browser/devtools capture. The server reads it so the page bytes never enter the model context.

baseUrl

Optional origin. Never fetched; used for pagination/gating detection only.

selectors

Same include/exclude shape as extract — applied at the normalize step so the diagnosis matches what extract would see.

topN

5

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

text (required)

Already-extracted text to split (e.g. markdown from extract). No HTML parsing or Readability scoring — the input is chunked verbatim.

maxTokens

500

Per-chunk token budget. No chunk exceeds this; oversized blocks are split by line, then hard-split.

overlap

0

Tokens to overlap between consecutive chunks (>=0). The trailing overlapChars of chunk N becomes the leading context of chunk N+1.

strategy

semantic

Chunking strategy. semantic (default) breaks on heading/section boundaries and never splits a fenced code block (an oversized code block is emitted as its own chunk that may exceed the budget — the deliberate tradeoff for keeping fences intact); char is the greedy char-bounded fallback that may split a code block.

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

text (required)

Markdown or text to summarize. Passed through to the host model verbatim; the server does not parse or modify it.

maxTokens

512

Upper bound on the summary length in tokens, forwarded as sampling/createMessage maxTokens. The host chooses the actual length.

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 generic lang-X / brush: X. Common language tokens are added to Readability's classesToPreserve so ```js/```shell land in the markdown instead of a bare fence; exotic languages fall back to an untagged fence. Automatic (no option); html_to_markdown is 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]: definition block. 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 .katex with 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-devtools evaluate_script (with its filePath arg), write document.head.outerHTML (for metadata) plus a content subtree such as document.querySelector('article')?.outerHTML || document.querySelector('main')?.outerHTML to a file, then point extract at it via localPath. baseUrl absolutizes 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, writes document.documentElement.outerHTML to a file via evaluate_script's filePath arg, then the readability extract tool reads that file via localPath and 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. The url argument is carried into the recipe as baseUrl (origin context) for extract.

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 cached extract result: hit:true/false, plus both hashes. The normalizedHash is what the key is built from; originalHash is the SHA-256 of the raw HTML. A miss where normalizedHash matches an existing entry but the lookup still missed points at an args-fingerprint mismatch (different format/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-render nonce= attributes, build-tool generated attribute names (data-v-…, data-css-…, data-svelte-…, data-h-…), and React/Next generated ids (: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/list enumerates current cache entries (readability://page/{cacheKey}, text/markdown); resources/read on a readability://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]
  • extract is the only subcommand; everything after it is parsed as options. With no args at all (readability-mcp), the stdio MCP server starts instead.

  • file.html is read from disk; when no file is given, HTML is read from stdin.

  • --format: md (default, markdown) | json (the structuredContent object, pretty-printed) | html (the post-pipeline HTML). Internally json reuses the markdown pipeline and serializes the structured object on the way out.

  • --max-chars N mirrors extract's maxChars — 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 extract

Development

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 run

Benchmark

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

Available Tools

3 tools
extractExtract article to MarkdownA

Extract the main article from already-rendered (post-JavaScript) HTML and return clean Markdown plus metadata and diagnostics. The server fetches nothing: html is the only source, and url (optional) is used solely to absolutize relative links. Hand it the output of document.documentElement.outerHTML from a browser/devtools capture.

ParametersJSON Schema
NameRequiredDescriptionDefault
gfmNoEnable GitHub-Flavored Markdown: tables, strikethrough, and task lists.
urlNoOrigin URL for absolutizing relative links and images. NEVER fetched — origin context only.
htmlYesAlready-rendered HTML (post-JavaScript), e.g. the result of document.documentElement.outerHTML from a browser/devtools capture. This is the ONLY input the server reads; it makes no outbound requests.
formatNoReturned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics}).markdown
imagesNoImage handling: 'keep' (inline ![alt](url)), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style).keep
maxCharsNoTruncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats.
maxNodesNoHard cap on elements parsed (Readability maxElemsToParse). Safety/perf guard for very large documents.
sanitizeNoRun DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes).
selectorsNoScope the extracted/converted content by CSS selector before processing.
extractionNoReadability scoring aggressiveness: 'balanced' (default), 'aggressive', or 'conservative'. Maps to Readability's scorer knobs.balanced
cleanChromeNoStrip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.
keepClassesNoRetain all CSS classes on extracted nodes. Defaults false, which strips non-language classes.
headingStyleNoMarkdown heading style: 'atx' (#) or 'setext' (underlining with = / -).atx
metadataModeNoPrepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'.none
codeBlockStyleNoMarkdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space).fenced
wordsPerMinuteNoReading speed (words per minute) used to compute metadata.readingTimeMin.
minArticleLengthNoMinimum article character length below which extraction falls back to the selector cascade (Readability charThreshold).
readabilityOverridesNoEscape hatch: a record spread verbatim into the Readability options. Unstable and unvalidated; overrides the extraction/keepClasses/maxNodes/minArticleLength knobs.

Output Schema

ParametersJSON Schema
NameRequiredDescription
contentYesThe human/LLM-readable payload — Markdown/html/text, or the serialized JSON when format=json.
metadataYesResolved article metadata. Each field is the first non-empty value across a priority cascade.
diagnosticsYesPipeline telemetry describing what was extracted, sanitized, and removed.
schemaVersionYesStructured-content schema version. Bumps only on breaking shape changes to this object.

TDQS

A4.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It discloses that the server makes no outbound requests and that url is only for absolutizing links. However, it does not mention failure modes, performance traits, or safety (e.g., no destructive actions). The description is adequate but could be more explicit about behavioral expectations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: two sentences that immediately convey purpose, input constraints, and practical usage. No wasted words, front-loaded with key information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's complexity (18 parameters, nested objects) and presence of an output schema, the description is sufficiently complete for an agent to understand when and how to use it. It covers the core purpose and input preparation. Could mention that it uses Readability under the hood, but not necessary.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds value by explaining the overall usage (e.g., 'Hand it the output of ...' for html parameter) and reinforcing constraints. This goes beyond the schema descriptions, justifying a higher score.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool extracts the main article from rendered HTML and returns clean Markdown plus metadata. It specifies the verb 'extract', the resource 'main article', and output format. It also distinguishes from siblings like html_to_markdown by focusing on article extraction, not arbitrary conversion.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description gives clear context: use when you have post-JavaScript HTML from browser devtools, and the server fetches nothing. It provides guidance on how to prepare input (use document.documentElement.outerHTML). However, it does not explicitly state when NOT to use or contrast with siblings beyond implicit differentiation.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

html_to_markdownConvert HTML fragment to MarkdownA

Convert an arbitrary HTML fragment to Markdown WITHOUT Readability article extraction (e.g. a snippet already isolated via chrome-devtools). Same Turndown + DOMPurify path as extract. The server fetches nothing: html is the only source, and url (optional) absolutizes relative links.

ParametersJSON Schema
NameRequiredDescriptionDefault
gfmNoEnable GitHub-Flavored Markdown: tables, strikethrough, and task lists.
urlNoOrigin URL for absolutizing relative links and images. NEVER fetched — origin context only.
htmlYesHTML fragment to convert to Markdown. No Readability article scoring is applied — the fragment is normalized and converted as-is.
formatNoReturned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics}).markdown
imagesNoImage handling: 'keep' (inline ![alt](url)), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style).keep
maxCharsNoTruncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats.
sanitizeNoRun DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes).
selectorsNoScope the extracted/converted content by CSS selector before processing.
cleanChromeNoStrip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.
headingStyleNoMarkdown heading style: 'atx' (#) or 'setext' (underlining with = / -).atx
metadataModeNoPrepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'.none
codeBlockStyleNoMarkdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space).fenced
wordsPerMinuteNoReading speed (words per minute) used to compute metadata.readingTimeMin.

Output Schema

ParametersJSON Schema
NameRequiredDescription
contentYesThe human/LLM-readable payload — Markdown/html/text, or the serialized JSON when format=json.
metadataYesResolved article metadata. Each field is the first non-empty value across a priority cascade.
diagnosticsYesPipeline telemetry describing what was extracted, sanitized, and removed.
schemaVersionYesStructured-content schema version. Bumps only on breaking shape changes to this object.

TDQS

A4.2/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries full burden. It mentions using the same Turndown + DOMPurify path as `extract`, implying sanitization, but does not disclose any potential side effects, error behavior, or edge cases. Adequate but minimal.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, both front-loaded with the core differentiator (no Readability) and usage context. Every word earns its place; no fluff.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool has 13 parameters, an output schema, and sibling tools, the description is brief but hits the critical distinction. It explains the input constraints and the relationship to `extract`. Some edge cases (e.g., format handling, error scenarios) are left implicit, but output schema covers return structure. Adequate for the complexity.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds modest value by clarifying that `url` is only for absolutizing relative links and that the server does not fetch it, which reinforces the 'no fetch' behavior. No other parameter details beyond schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool converts HTML fragments to Markdown without Readability extraction, and explicitly distinguishes it from the sibling tool `extract` by specifying the use case (already isolated snippet).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly tells when to use (snippet isolated via chrome-devtools) and when not to (no Readability article extraction), and directly references the sibling tool `extract` as the alternative. The 'server fetches nothing' line further clarifies input constraints.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

outlineGet document outline (heading TOC)A

Return the document outline (h1-h6 headings with stable anchor ids) of already-rendered (post-JavaScript) HTML as a cheap pre-check before full extraction. No Readability scoring, no Turndown, no sanitization — a pure heading walk. The server fetches nothing: html is the only source, and url is origin context only (never fetched).

ParametersJSON Schema
NameRequiredDescriptionDefault
urlNoOrigin URL, carried through to metadata.url and used to absolutize links. NEVER fetched — origin context only.
htmlYesAlready-rendered HTML (post-JavaScript) to walk for headings. No Readability scoring, Turndown, or sanitization is applied.

Output Schema

ParametersJSON Schema
NameRequiredDescription
contentYesIndented-bullet table of contents, one line per heading, nested by depth.
outlineYesDocument headings (h1–h6) in document order, each with a stable anchor id.
metadataYesOutline document metadata.
schemaVersionYesStructured-content schema version. Bumps only on breaking shape changes to this object.

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description fully discloses key behaviors: the server never fetches any external resources, 'html' is the sole data source, and 'url' is only used for origin context and absolutizing links. It also notes the absence of transformations like Readability or Turndown. This provides a comprehensive behavioral picture.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise: two sentences that front-load the purpose and then quickly detail behavioral constraints. No superfluous words; every sentence adds unique value. The structure is efficient and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (2 parameters, 1 required, output schema present), the description covers all necessary aspects: purpose, usage context, behavioral traits, parameter roles, and relationship to siblings. It is fully sufficient for an AI agent to select and invoke the tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema covers both parameters with descriptions (100% coverage), meeting the baseline of 3. The description adds significant context beyond the schema: 'url' is 'NEVER fetched — origin context only', and 'html' is already-rendered and used unmodified. This additional semantic clarity justifies a score of 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's function: returning a document outline (h1-h6 headings with stable anchor ids) from already-rendered HTML. It distinguishes itself from siblings by emphasizing it is a 'pure heading walk' with no Readability, Turndown, or sanitization, making the purpose specific and non-ambiguous.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description positions the tool as a 'cheap pre-check before full extraction', implying it should be used when only headings are needed. It contrasts with extraction tools by listing what it omits (Readability, Turndown, sanitization). However, it does not explicitly name sibling tools or state when not to use it, but the context is clear enough.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 3 tool updatesv0.5.0
    • Changedextract45 fields changed
      • addedInput schema / properties / cleanChrome
        Added value: +{
        +  "default": true,
        +  "description": "Strip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / codeBlockStyle / description
        Added value: +"Markdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space)."
      • addedInput schema / properties / extraction / description
        Added value: +"Readability scoring aggressiveness: 'balanced' (default), 'aggressive', or 'conservative'. Maps to Readability's scorer knobs."
      • addedInput schema / properties / format / description
        Added value: +"Returned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics})."
      • addedInput schema / properties / gfm / description
        Added value: +"Enable GitHub-Flavored Markdown: tables, strikethrough, and task lists."
      • addedInput schema / properties / headingStyle / description
        Added value: +"Markdown heading style: 'atx' (#) or 'setext' (underlining with = / -)."
      • addedInput schema / properties / html / description
        Added value: +"Already-rendered HTML (post-JavaScript), e.g. the result of document.documentElement.outerHTML from a browser/devtools capture. This is the ONLY input the server reads; it makes no outbound requests."
      • addedInput schema / properties / images / description
        Added value: +"Image handling: 'keep' (inline ![alt](url)), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style)."
      • addedInput schema / properties / keepClasses / description
        Added value: +"Retain all CSS classes on extracted nodes. Defaults false, which strips non-language classes."
      • addedInput schema / properties / maxChars / description
        Added value: +"Truncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats."
      • addedInput schema / properties / maxNodes / description
        Added value: +"Hard cap on elements parsed (Readability maxElemsToParse). Safety/perf guard for very large documents."
      • addedInput schema / properties / metadataMode / description
        Added value: +"Prepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'."
      • addedInput schema / properties / minArticleLength / description
        Added value: +"Minimum article character length below which extraction falls back to the selector cascade (Readability charThreshold)."
      • addedInput schema / properties / readabilityOverrides / description
        Added value: +"Escape hatch: a record spread verbatim into the Readability options. Unstable and unvalidated; overrides the extraction/keepClasses/maxNodes/minArticleLength knobs."
      • addedInput schema / properties / sanitize / description
        Added value: +"Run DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes)."
      • addedInput schema / properties / selectors / description
        Added value: +"Scope the extracted/converted content by CSS selector before processing."
      • addedInput schema / properties / selectors / properties / exclude / description
        Added value: +"CSS selectors for boilerplate to remove before extraction (e.g. [\"nav\", \"footer\", \"[role=banner]\"])."
      • addedInput schema / properties / selectors / properties / include / description
        Added value: +"CSS selector restricting extraction to a matching subtree (e.g. \"main\", \"article\", \".post\"). The first match replaces the document body before processing."
      • addedInput schema / properties / url / description
        Added value: +"Origin URL for absolutizing relative links and images. NEVER fetched — origin context only."
      • addedInput schema / properties / wordsPerMinute / description
        Added value: +"Reading speed (words per minute) used to compute metadata.readingTimeMin."
      • addedOutput schema / properties / content / description
        Added value: +"The human/LLM-readable payload — Markdown/html/text, or the serialized JSON when format=json."
      • addedOutput schema / properties / diagnostics / description
        Added value: +"Pipeline telemetry describing what was extracted, sanitized, and removed."
      • addedOutput schema / properties / diagnostics / properties / chromeRemoved
        Added value: +{
        +  "description": "Count of browser-chrome nodes stripped before conversion (scrollbars, consent banners, overlays).",
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / diagnostics / properties / extractedNode / description
        Added value: +"DOM root extraction came from: \"readability\" (main path), a fallback selector (e.g. \"article\", \"main\"), or \"fragment\" for html_to_markdown."
      • addedOutput schema / properties / diagnostics / properties / fallbackUsed / description
        Added value: +"True if Readability parse failed and a selector cascade salvaged content. Always true for html_to_markdown."
      • addedOutput schema / properties / diagnostics / properties / imagesResolved / description
        Added value: +"Count of lazy/placeholder images resolved to their real src before conversion."
      • addedOutput schema / properties / diagnostics / properties / readerable / description
        Added value: +"Readability isProbablyReaderable verdict on the document (extract main path only)."
      • addedOutput schema / properties / diagnostics / properties / removedNodes / description
        Added value: +"Net element count removed across the whole pipeline (delta vs. the parsed document)."
      • addedOutput schema / properties / diagnostics / properties / sanitization / description
        Added value: +"Counts of nodes removed by DOMPurify sanitization."
      • addedOutput schema / properties / diagnostics / properties / sanitization / properties / iframes / description
        Added value: +"<iframe> elements removed by sanitization."
      • addedOutput schema / properties / diagnostics / properties / sanitization / properties / scripts / description
        Added value: +"<script> and event-handler nodes removed by sanitization."
      • addedOutput schema / properties / diagnostics / properties / truncated / description
        Added value: +"True if the payload was truncated by maxChars."
      • addedOutput schema / properties / metadata / description
        Added value: +"Resolved article metadata. Each field is the first non-empty value across a priority cascade."
      • addedOutput schema / properties / metadata / properties / byline / description
        Added value: +"Article author(s), resolved from JSON-LD, OpenGraph, <meta>, or Readability."
      • addedOutput schema / properties / metadata / properties / estimator
        Added value: +{
        +  "description": "Name of the heuristic backing tokenEstimate (e.g. \"chars/4\").",
        +  "type": "string"
        +}
      • addedOutput schema / properties / metadata / properties / excerpt / description
        Added value: +"Short article summary produced by Readability."
      • addedOutput schema / properties / metadata / properties / lang / description
        Added value: +"Detected document language."
      • addedOutput schema / properties / metadata / properties / publishedTime / description
        Added value: +"Publication timestamp resolved from JSON-LD, <meta>, or <time> elements."
      • addedOutput schema / properties / metadata / properties / readingTimeMin / description
        Added value: +"Estimated reading time in minutes, derived from wordCount and wordsPerMinute."
      • addedOutput schema / properties / metadata / properties / siteName / description
        Added value: +"Publishing site name, resolved from OpenGraph or <meta>."
      • addedOutput schema / properties / metadata / properties / title / description
        Added value: +"Article title, resolved by priority cascade (JSON-LD → OpenGraph → Twitter → <meta> → Readability → <title>)."
      • addedOutput schema / properties / metadata / properties / tokenEstimate
        Added value: +{
        +  "description": "Rough output token count (chars/4 by default) for context budgeting.",
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / metadata / properties / url / description
        Added value: +"The url passed in (origin context), or the article canonical URL when discoverable."
      • addedOutput schema / properties / metadata / properties / wordCount / description
        Added value: +"Number of whitespace-separated words in the extracted text."
      • addedOutput schema / properties / schemaVersion / description
        Added value: +"Structured-content schema version. Bumps only on breaking shape changes to this object."
    • Changedhtml_to_markdown40 fields changed
      • addedInput schema / properties / cleanChrome
        Added value: +{
        +  "default": true,
        +  "description": "Strip browser chrome (scrollbars, consent/cookie banners, fixed nav and overlays) before conversion. These elements poison Readability density scoring and clutter fragment output.",
        +  "type": "boolean"
        +}
      • addedInput schema / properties / codeBlockStyle / description
        Added value: +"Markdown code-block style: 'fenced' (triple backticks) or 'indented' (four-space)."
      • addedInput schema / properties / format / description
        Added value: +"Returned payload format: 'markdown' (default), 'html', 'text', or 'json' (emits {metadata, content, diagnostics})."
      • addedInput schema / properties / gfm / description
        Added value: +"Enable GitHub-Flavored Markdown: tables, strikethrough, and task lists."
      • addedInput schema / properties / headingStyle / description
        Added value: +"Markdown heading style: 'atx' (#) or 'setext' (underlining with = / -)."
      • addedInput schema / properties / html / description
        Added value: +"HTML fragment to convert to Markdown. No Readability article scoring is applied — the fragment is normalized and converted as-is."
      • addedInput schema / properties / images / description
        Added value: +"Image handling: 'keep' (inline ![alt](url)), 'drop', 'src-only' (bare URL text), or 'reference' (link-reference style)."
      • addedInput schema / properties / maxChars / description
        Added value: +"Truncate markdown/text output at a block boundary; never splits a fenced code block. Ignored for html/json formats."
      • addedInput schema / properties / metadataMode / description
        Added value: +"Prepend a metadata block to the markdown/text payload: 'none' (default), 'yaml', or 'json'."
      • addedInput schema / properties / sanitize / description
        Added value: +"Run DOMPurify over the extracted/fragment HTML before conversion (strips scripts, event handlers, and iframes)."
      • addedInput schema / properties / selectors / description
        Added value: +"Scope the extracted/converted content by CSS selector before processing."
      • addedInput schema / properties / selectors / properties / exclude / description
        Added value: +"CSS selectors for boilerplate to remove before extraction (e.g. [\"nav\", \"footer\", \"[role=banner]\"])."
      • addedInput schema / properties / selectors / properties / include / description
        Added value: +"CSS selector restricting extraction to a matching subtree (e.g. \"main\", \"article\", \".post\"). The first match replaces the document body before processing."
      • addedInput schema / properties / url / description
        Added value: +"Origin URL for absolutizing relative links and images. NEVER fetched — origin context only."
      • addedInput schema / properties / wordsPerMinute / description
        Added value: +"Reading speed (words per minute) used to compute metadata.readingTimeMin."
      • addedOutput schema / properties / content / description
        Added value: +"The human/LLM-readable payload — Markdown/html/text, or the serialized JSON when format=json."
      • addedOutput schema / properties / diagnostics / description
        Added value: +"Pipeline telemetry describing what was extracted, sanitized, and removed."
      • addedOutput schema / properties / diagnostics / properties / chromeRemoved
        Added value: +{
        +  "description": "Count of browser-chrome nodes stripped before conversion (scrollbars, consent banners, overlays).",
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / diagnostics / properties / extractedNode / description
        Added value: +"DOM root extraction came from: \"readability\" (main path), a fallback selector (e.g. \"article\", \"main\"), or \"fragment\" for html_to_markdown."
      • addedOutput schema / properties / diagnostics / properties / fallbackUsed / description
        Added value: +"True if Readability parse failed and a selector cascade salvaged content. Always true for html_to_markdown."
      • addedOutput schema / properties / diagnostics / properties / imagesResolved / description
        Added value: +"Count of lazy/placeholder images resolved to their real src before conversion."
      • addedOutput schema / properties / diagnostics / properties / readerable / description
        Added value: +"Readability isProbablyReaderable verdict on the document (extract main path only)."
      • addedOutput schema / properties / diagnostics / properties / removedNodes / description
        Added value: +"Net element count removed across the whole pipeline (delta vs. the parsed document)."
      • addedOutput schema / properties / diagnostics / properties / sanitization / description
        Added value: +"Counts of nodes removed by DOMPurify sanitization."
      • addedOutput schema / properties / diagnostics / properties / sanitization / properties / iframes / description
        Added value: +"<iframe> elements removed by sanitization."
      • addedOutput schema / properties / diagnostics / properties / sanitization / properties / scripts / description
        Added value: +"<script> and event-handler nodes removed by sanitization."
      • addedOutput schema / properties / diagnostics / properties / truncated / description
        Added value: +"True if the payload was truncated by maxChars."
      • addedOutput schema / properties / metadata / description
        Added value: +"Resolved article metadata. Each field is the first non-empty value across a priority cascade."
      • addedOutput schema / properties / metadata / properties / byline / description
        Added value: +"Article author(s), resolved from JSON-LD, OpenGraph, <meta>, or Readability."
      • addedOutput schema / properties / metadata / properties / estimator
        Added value: +{
        +  "description": "Name of the heuristic backing tokenEstimate (e.g. \"chars/4\").",
        +  "type": "string"
        +}
      • addedOutput schema / properties / metadata / properties / excerpt / description
        Added value: +"Short article summary produced by Readability."
      • addedOutput schema / properties / metadata / properties / lang / description
        Added value: +"Detected document language."
      • addedOutput schema / properties / metadata / properties / publishedTime / description
        Added value: +"Publication timestamp resolved from JSON-LD, <meta>, or <time> elements."
      • addedOutput schema / properties / metadata / properties / readingTimeMin / description
        Added value: +"Estimated reading time in minutes, derived from wordCount and wordsPerMinute."
      • addedOutput schema / properties / metadata / properties / siteName / description
        Added value: +"Publishing site name, resolved from OpenGraph or <meta>."
      • addedOutput schema / properties / metadata / properties / title / description
        Added value: +"Article title, resolved by priority cascade (JSON-LD → OpenGraph → Twitter → <meta> → Readability → <title>)."
      • addedOutput schema / properties / metadata / properties / tokenEstimate
        Added value: +{
        +  "description": "Rough output token count (chars/4 by default) for context budgeting.",
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
      • addedOutput schema / properties / metadata / properties / url / description
        Added value: +"The url passed in (origin context), or the article canonical URL when discoverable."
      • addedOutput schema / properties / metadata / properties / wordCount / description
        Added value: +"Number of whitespace-separated words in the extracted text."
      • addedOutput schema / properties / schemaVersion / description
        Added value: +"Structured-content schema version. Bumps only on breaking shape changes to this object."
    • Changedoutline12 fields changed
      • addedInput schema / properties / html / description
        Added value: +"Already-rendered HTML (post-JavaScript) to walk for headings. No Readability scoring, Turndown, or sanitization is applied."
      • addedInput schema / properties / url / description
        Added value: +"Origin URL, carried through to metadata.url and used to absolutize links. NEVER fetched — origin context only."
      • addedOutput schema / properties / content / description
        Added value: +"Indented-bullet table of contents, one line per heading, nested by depth."
      • addedOutput schema / properties / metadata / description
        Added value: +"Outline document metadata."
      • addedOutput schema / properties / metadata / properties / title / description
        Added value: +"Document title from <title>, falling back to the first <h1>."
      • addedOutput schema / properties / metadata / properties / url / description
        Added value: +"The url passed in (origin context, never fetched)."
      • addedOutput schema / properties / outline / description
        Added value: +"Document headings (h1–h6) in document order, each with a stable anchor id."
      • addedOutput schema / properties / outline / items / description
        Added value: +"A single document heading with its stable anchor."
      • addedOutput schema / properties / outline / items / properties / anchor / description
        Added value: +"Stable anchor id: the heading own id, a descendant permalink fragment, or a slug of the text (deduped -1, -2, … for generated slugs)."
      • addedOutput schema / properties / outline / items / properties / level / description
        Added value: +"Heading level (1–6)."
      • addedOutput schema / properties / outline / items / properties / text / description
        Added value: +"Heading text content."
      • addedOutput schema / properties / schemaVersion / description
        Added value: +"Structured-content schema version. Bumps only on breaking shape changes to this object."
  2. 3 tool updatesv0.4.0
    • Changedextract1 field changed
      • addedOutput schema / properties / diagnostics / properties / imagesResolved
        Added value: +{
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
    • Changedhtml_to_markdown1 field changed
      • addedOutput schema / properties / diagnostics / properties / imagesResolved
        Added value: +{
        +  "maximum": 9007199254740991,
        +  "minimum": -9007199254740991,
        +  "type": "integer"
        +}
    • Addedoutline
  3. 2 tool updatesv0.1.0
    • First observedextract
    • First observedhtml_to_markdown

TDQS

A4.4/5.0
Disambiguation5/5

Each tool has a distinct purpose: 'extract' performs full article extraction with Readability, 'html_to_markdown' converts arbitrary HTML without extraction, and 'outline' provides a heading-based preview. No overlap exists, making it easy for agents to select the right tool.

Naming Consistency4/5

The names are all lowercase with underscores and convey the action clearly, but the patterns differ: 'extract' is a verb, 'html_to_markdown' describes the transformation, and 'outline' is a noun. Minor inconsistency, but overall predictable.

Tool Count5/5

With 3 tools, the server covers the essential operations for HTML-to-Markdown conversion: article extraction, arbitrary snippet conversion, and outline preview. The count feels well-scoped and reasonable for the domain.

Completeness4/5

The tool surface covers the primary use cases. A minor gap is the lack of a tool to fetch raw HTML from a URL, but the server explicitly declares it does not fetch, so that's by design. Including diagnostics in 'extract' partially compensates.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Converts 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.
    2
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    Converts any webpage into clean, LLM-ready Markdown, removing noise and supporting JavaScript rendering.
    MIT

Latest Blog Posts

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