@v1nvn/readability-mcp 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +310 -0
- package/dist/assets/cli-BwKCixh6.js +83 -0
- package/dist/assets/cli-BwKCixh6.js.map +1 -0
- package/dist/assets/extract-BKl4PzEI.js +2734 -0
- package/dist/assets/extract-BKl4PzEI.js.map +1 -0
- package/dist/index.js +1425 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/sampling.ts","../src/tools/chunk_text.ts","../src/policy/explain.ts","../src/tools/explain.ts","../src/policy/sibling-scan.ts","../src/policy/grid-detector.ts","../src/tools/extract_grid.ts","../src/tools/extract_links.ts","../src/policy/list-detector.ts","../src/tools/extract_list.ts","../src/tools/extract_metadata.ts","../src/policy/outline.ts","../src/policy/section.ts","../src/tools/extract_section.ts","../src/tools/extract_tables.ts","../src/tools/html_to_markdown.ts","../src/tools/outline.ts","../src/server.ts","../src/index.ts"],"sourcesContent":["// Optional, capability-gated features backed by the HOST's model via MCP\n// `sampling/createMessage` (server→client request). The server never embeds a\n// model and never calls a provider directly — every LLM call is delegated to\n// the connected client, which picks the model and may prompt the user first.\n\nimport { z } from 'zod';\n\nimport type { ToolHandle } from './server.js';\n\nimport { toErrorResult } from './errors.js';\nimport { logger } from './logger.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst SUMMARIZE_SYSTEM_PROMPT =\n 'Summarize the user-supplied text concisely while preserving its key points, entities, and any decisive conclusions. Output only the summary prose — no preamble, no headings unless the source had them.';\n\nconst summarizeInputShape = {\n text: z\n .string()\n .describe(\n 'The markdown or text to summarize — typically the output of `extract`, `extract_section`, `html_to_markdown`, or `chunk_text`. Passed through to the host model verbatim; the server does not parse or modify it.',\n ),\n maxTokens: z\n .number()\n .int()\n .min(1)\n .describe(\n 'Upper bound on the summary length in tokens, forwarded to the host as `sampling/createMessage` maxTokens. The host chooses the actual length.',\n )\n .default(512),\n} as const;\n\nconst summarizeInputSchema = z.object(summarizeInputShape);\n\nexport const SUMMARIZE_TOOL_DESCRIPTION = `Summarize text using the HOST's model via MCP \\`sampling/createMessage\\` — the server embeds no model and calls no provider directly. Hand it the output of \\`extract\\`, \\`extract_section\\`, \\`html_to_markdown\\`, or any markdown/text string; the host picks the model and may ask the user to approve the sampling request (human-in-the-loop per MCP). The tool is only listed when the connected client advertises the sampling capability.`;\n\nasync function summarizeWithHost(\n server: McpServer,\n args: { maxTokens: number; text: string },\n): Promise<string> {\n const result = await server.server.createMessage({\n messages: [\n {\n role: 'user',\n content: { type: 'text', text: args.text },\n },\n ],\n systemPrompt: SUMMARIZE_SYSTEM_PROMPT,\n maxTokens: args.maxTokens,\n });\n if (result.content.type !== 'text') {\n throw new Error(\n `host sampling returned non-text content (${result.content.type}); summarize expects a text response`,\n );\n }\n return result.content.text;\n}\n\nexport function registerSummarizeTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'summarize',\n {\n title: 'Summarize text using the host model',\n description: SUMMARIZE_TOOL_DESCRIPTION,\n inputSchema: summarizeInputShape,\n },\n async (rawArgs: unknown): Promise<CallToolResult> => {\n const args = summarizeInputSchema.parse(rawArgs);\n try {\n const summary = await summarizeWithHost(server, args);\n return {\n content: [{ type: 'text', text: summary }],\n };\n } catch (err) {\n logger.error(\n `summarize failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n }\n },\n );\n}\n\n// Mirrors registerTools/registerResources so the dev reload\n// loop and capability gate can treat sampling as one registration family.\nexport function registerSamplingTools(server: McpServer): ToolHandle[] {\n return [registerSummarizeTool(server)];\n}\n","import type { Chunk } from '../policy/chunk.js';\nimport type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { chunkMarkdown } from '../policy/chunk.js';\nimport { chunkTextOutputShape } from './output-schema.js';\nimport { chunkTextInputSchema, chunkTextInputShape } from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n// One numbered section per chunk, heading context in brackets when present, so\n// content[0].text is always scannable without unpacking structuredContent.\nfunction renderChunkIndex(chunks: readonly Chunk[]): string {\n if (chunks.length === 0) {\n return '(no chunks emitted — input had no non-whitespace content)';\n }\n return chunks\n .map(chunk => {\n const head = chunk.headingContext ? ` [${chunk.headingContext}]` : '';\n return `## Chunk ${chunk.index}${head}\\n\\n${chunk.text}`;\n })\n .join('\\n\\n');\n}\n\nexport function chunkTextDocument(rawArgs: unknown): CallToolResult {\n const args = chunkTextInputSchema.parse(rawArgs);\n const { text, maxTokens, overlap, strategy } = args;\n const chunks = chunkMarkdown(text, { maxTokens, overlap, strategy });\n const content = renderChunkIndex(chunks);\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n chunks,\n },\n };\n}\n\nexport const CHUNK_TEXT_TOOL_DESCRIPTION = `Split already-extracted text into token-bounded chunks for embedding/RAG. Each chunk carries its index, tokenCount (chars/4), and the nearest preceding markdown heading as headingContext. Operates on any text — pair with the \\`chunk\\` option on \\`extract\\` when you want chunks inline with the extraction. The server fetches nothing: \\`text\\` is the only input.`;\n\nexport function chunkTextHandler(args: unknown): CallToolResult {\n try {\n return chunkTextDocument(args);\n } catch (err) {\n logger.error(\n `chunk_text failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerChunkTextTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'chunk_text',\n {\n title: 'Chunk text for RAG/embedding',\n description: CHUNK_TEXT_TOOL_DESCRIPTION,\n inputSchema: chunkTextInputShape,\n outputSchema: chunkTextOutputShape,\n },\n chunkTextHandler,\n );\n}\n","import { Readability } from '@mozilla/readability';\n\nimport type { GatingSignal } from './gating.js';\nimport type { PaginationSignal } from './pagination.js';\n\nimport { buildDocument } from '../pipeline/dom.js';\nimport {\n applySelectors,\n normalizeDocument,\n resolveLazyImages,\n type SelectorScope,\n} from '../pipeline/normalize.js';\nimport { isReaderable } from '../pipeline/readability.js';\nimport { assembleDiagnostics } from './diagnostics.js';\nimport { detectGating } from './gating.js';\nimport { detectPagination } from './pagination.js';\nimport { resolveReadabilityOptions } from './resolver.js';\n\n// Readability stamps `{ contentScore }` on candidate DOM nodes under a\n// `readability` expando (Readability.js:894/1272/1288). The property is\n// untyped upstream, so reach it through `unknown` rather than a guessed shape.\ninterface ReadabilityExpando {\n readonly contentScore?: number;\n}\n\nexport interface ExplainCandidate {\n readonly className: string;\n readonly id: string;\n readonly score: number;\n readonly selector: string;\n readonly tag: string;\n readonly textLength: number;\n}\n\nexport interface ExplainRemovedNodes {\n readonly boilerplate: number;\n readonly chrome: number;\n readonly total: number;\n}\n\nexport interface ExplainSnapshot {\n readonly html: string;\n readonly truncated: boolean;\n}\n\nexport interface ExplainReport {\n readonly candidates: readonly ExplainCandidate[];\n readonly chosenRoot: ExplainCandidate | null;\n readonly fallbackUsed: boolean;\n readonly gating: GatingSignal | undefined;\n readonly pagination: PaginationSignal | undefined;\n readonly parseSucceeded: boolean;\n readonly readerable: boolean;\n readonly removedNodes: ExplainRemovedNodes;\n readonly snapshot: ExplainSnapshot;\n}\n\nexport interface BuildExplainOptions {\n readonly baseUrl?: string;\n readonly html: string;\n readonly selectors?: Readonly<SelectorScope>;\n readonly snapshotMaxChars?: number;\n readonly topN?: number;\n}\n\nconst DEFAULT_SNAPSHOT_MAX = 4000;\nconst DEFAULT_TOP_N = 5;\n\nfunction describeSelector(parts: {\n className: string;\n id: string;\n tag: string;\n}): string {\n // A CSS-ish hint for the host, not a unique locator — Readability's expando is\n // a JS-only property invisible to CSS, and we deliberately avoid an nth-child\n // chain that would be brittle against the host's live DOM. Tag + id + first\n // two classes is enough for a human to pick the node out of a small candidate\n // list.\n const idPart = parts.id ? `#${parts.id}` : '';\n const cls = parts.className\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .join('.');\n const classPart = cls ? `.${cls}` : '';\n return `${parts.tag.toLowerCase()}${idPart}${classPart}`;\n}\n\nfunction readScored(el: Element): ExplainCandidate | null {\n const stamp = (el as unknown as { readability?: ReadabilityExpando })\n .readability;\n const score = stamp?.contentScore;\n if (typeof score !== 'number') {\n return null;\n }\n const className = typeof el.className === 'string' ? el.className : '';\n const candidate = {\n className,\n id: el.id,\n score,\n tag: el.tagName,\n textLength: el.textContent.trim().length,\n };\n return { ...candidate, selector: describeSelector(candidate) };\n}\n\nfunction truncateSnapshot(html: string, max: number): ExplainSnapshot {\n if (html.length <= max) {\n return { html, truncated: false };\n }\n return { html: html.slice(0, max), truncated: true };\n}\n\nexport function buildExplainReport(\n options: Readonly<BuildExplainOptions>,\n): ExplainReport {\n const {\n html,\n selectors,\n snapshotMaxChars = DEFAULT_SNAPSHOT_MAX,\n topN = DEFAULT_TOP_N,\n baseUrl,\n } = options;\n\n const { document, window } = buildDocument(html, baseUrl);\n\n // Mirror the extract pipeline's ordering: gating detection must precede\n // normalization (chrome stripping would remove the overlay), and pagination\n // detection runs before applySelectors (a caller's include could hide the\n // sentinel but not the \"more content exists\" signal).\n const gating = detectGating(document);\n const documentElementCount = document.querySelectorAll('*').length;\n const normalizeCounts = normalizeDocument(document);\n resolveLazyImages(document);\n const pagination = detectPagination(document, baseUrl);\n applySelectors(document, selectors);\n\n const snapshot = truncateSnapshot(document.body.innerHTML, snapshotMaxChars);\n\n const readerable = isReaderable(document);\n const readabilityOptions = resolveReadabilityOptions({});\n\n // Clone so the normalized doc (and the snapshot above) is preserved untouched\n // — Readability restructures its input during parse.\n const clone = document.cloneNode(true) as Document;\n // Grab node references before parse: Readability detaches scored candidates\n // from the live tree while restructuring, so a post-parse querySelectorAll\n // finds almost nothing. The object refs held here keep their `readability`\n // stamps after being detached — that retention is what surfaces the real\n // per-candidate scores without forking the library or parsing its debug log.\n const heldNodes = Array.from(clone.querySelectorAll('*'));\n\n const reader = new Readability(clone, readabilityOptions);\n const article = reader.parse();\n const parseSucceeded = !!article?.content;\n\n const scored: ExplainCandidate[] = [];\n for (const el of heldNodes) {\n const entry = readScored(el);\n if (entry) {\n scored.push(entry);\n }\n }\n scored.sort((a, b) => b.score - a.score);\n\n const candidates = scored.slice(0, Math.max(0, topN));\n const chosenRoot = scored[0] ?? null;\n\n const diagnostics = assembleDiagnostics({\n articleHtml: article?.content ?? '',\n boilerplateRemoved: normalizeCounts.boilerplateRemoved,\n chromeRemoved: normalizeCounts.chromeRemoved,\n documentElementCount,\n extractedNode: 'readability',\n fallbackUsed: false,\n gated: gating,\n pagination,\n readerable,\n window,\n });\n\n return {\n candidates,\n chosenRoot,\n fallbackUsed: diagnostics.fallbackUsed,\n gating: diagnostics.gated,\n pagination: diagnostics.pagination,\n parseSucceeded,\n readerable: diagnostics.readerable ?? false,\n removedNodes: {\n boilerplate: diagnostics.boilerplateRemoved ?? 0,\n chrome: diagnostics.chromeRemoved ?? 0,\n total: diagnostics.removedNodes ?? 0,\n },\n snapshot,\n };\n}\n","import { z } from 'zod';\n\nimport type { ExplainReport } from '../policy/explain.js';\nimport type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildExplainReport } from '../policy/explain.js';\nimport { readHtmlFile } from './html-source.js';\nimport {\n type FromHtmlInput,\n localPathField,\n selectorsSchema,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n// Schemas live with the tool rather than in schemas.ts/output-schema.ts to keep\n// the explain surface in one file (the report shape is explain-specific and not\n// shared with other tools). `selectorsSchema` is reused verbatim from extract so\n// the two tools agree on the include/exclude contract.\nconst explainInputShape = {\n localPath: localPathField,\n baseUrl: z\n .url()\n .describe(\n 'Base URL for absolutizing relative links during pagination/gating detection. NEVER fetched — origin context only.',\n )\n .optional(),\n selectors: selectorsSchema,\n topN: z\n .number()\n .int()\n .min(1)\n .max(20)\n .describe(\n 'Maximum number of scored candidate nodes to return (highest first). Default 5.',\n )\n .default(5),\n} as const;\n\nconst explainInputSchema = z.object(explainInputShape);\n\ntype ExplainInput = z.infer<typeof explainInputSchema>;\ntype ExplainFromHtmlInput = FromHtmlInput<ExplainInput>;\n\n// Schema defaults for callers that pass only a subset of the knobs (topN).\nconst DEFAULTS: Omit<ExplainInput, 'localPath'> = explainInputSchema.parse({\n localPath: '',\n});\n\nconst candidateSchema = z\n .object({\n className: z\n .string()\n .describe(\n \"The candidate's class attribute (raw, unsplit). Empty string when absent.\",\n ),\n id: z.string().describe(\"The candidate's id attribute, or empty string.\"),\n score: z\n .number()\n .describe(\n \"Readability's actual contentScore for this node (link-density-scaled). Higher is better; the top entry is Readability's raw top candidate before its parent-walking/only-child adjustments.\",\n ),\n selector: z\n .string()\n .describe(\n \"A CSS-ish hint (tag#id.class1.class2) for locating the node in the host DOM. NOT a unique locator — Readability's score lives on a JS expando invisible to CSS.\",\n ),\n tag: z\n .string()\n .describe('Uppercase DOM tag name (e.g. \"ARTICLE\", \"MAIN\", \"DIV\").'),\n textLength: z\n .number()\n .int()\n .min(0)\n .describe(\n 'Trimmed textContent length of the candidate node, for eyeballing content density.',\n ),\n })\n .describe(\n 'One scored candidate node Readability considered, with its real contentScore.',\n );\n\nconst explainOutputShape = {\n schemaVersion: z\n .literal(1)\n .describe(\n 'Structured-content schema version. Bumps only on breaking shape changes to this object.',\n ),\n content: z\n .string()\n .describe(\n 'Readable rendering of the report (chosen root, ranked candidates, removal counts, gating/pagination, snapshot head) so content[0].text is always scannable.',\n ),\n chosenRoot: candidateSchema\n .nullable()\n .describe(\n 'The highest-scoring candidate Readability computed — its raw top pick before parent-walking/only-child post-processing. Null when Readability scored nothing (e.g. empty input).',\n ),\n candidates: z\n .array(candidateSchema)\n .describe(\n \"Scored candidate nodes (highest first), capped at topN. These are Readability's real contentScore values, not a self-computed heuristic.\",\n ),\n readerable: z\n .boolean()\n .describe(\n 'Readability isProbablyReaderable verdict on the normalized document.',\n ),\n parseSucceeded: z\n .boolean()\n .describe(\n 'True when reader.parse() returned article content. False signals that `extract` would fall back to its selector cascade.',\n ),\n fallbackUsed: z\n .boolean()\n .describe(\n 'Always false for explain — this tool runs only the Readability path it is diagnosing, never the fallback cascade.',\n ),\n gating: z\n .object({\n likely: z\n .boolean()\n .describe(\n 'True when heuristics strongly suggest the content is paywalled or truncated.',\n ),\n reason: z\n .string()\n .describe(\n 'Short label naming the detected signal (e.g. \"paywall overlay\").',\n ),\n })\n .nullable()\n .describe(\n 'Likely paywall / gating signal detected before normalization. Null when none.',\n ),\n pagination: z\n .object({\n type: z\n .enum(['infinite', 'paginated'])\n .describe('Kind of pagination signal detected.'),\n nextUrl: z\n .string()\n .optional()\n .describe(\n 'Absolute URL of the detected next page (paginated only). Never fetched.',\n ),\n selector: z\n .string()\n .optional()\n .describe(\n 'CSS selector of the load-more / infinite-scroll sentinel (infinite only).',\n ),\n })\n .nullable()\n .describe('Detected pagination / infinite-scroll signal. Null when none.'),\n removedNodes: z\n .object({\n boilerplate: z\n .number()\n .int()\n .min(0)\n .describe(\n 'Boilerplate blocks (related-posts, newsletter signup) stripped before Readability.',\n ),\n chrome: z\n .number()\n .int()\n .min(0)\n .describe(\n 'Browser-chrome nodes stripped (scrollbars, consent banners, overlays).',\n ),\n total: z\n .number()\n .int()\n .min(0)\n .describe(\n 'Net element count removed across the whole pipeline (delta vs. the parsed document).',\n ),\n })\n .describe(\n 'Breakdown of nodes removed before Readability saw the document, reused from the extract diagnostics path.',\n ),\n snapshot: z\n .object({\n html: z\n .string()\n .describe(\n 'The sanitized-by-normalization (post chrome/boilerplate/script strip) HTML fed to Readability — \"what Readability saw\". Not DOMPurify-sanitized (that runs on Readability\\'s output in `extract`), so it may still carry inline event handlers (`onerror`/`onclick`/…); it is diagnostic data — do not render verbatim.',\n ),\n truncated: z\n .boolean()\n .describe(\n 'True when the snapshot was cut at snapshotMaxChars (default 4000).',\n ),\n })\n .describe('Pre-Readability HTML snapshot of the normalized document body.'),\n} as const;\n\nconst explainOutput = z.object(explainOutputShape);\n\nfunction formatGating(report: ExplainReport): string {\n const g = report.gating;\n if (!g) {\n return 'none';\n }\n return `${g.reason}${g.likely ? '' : ' (weak)'}`;\n}\n\nfunction formatPagination(report: ExplainReport): string {\n const p = report.pagination;\n if (!p) {\n return 'none';\n }\n if (p.type === 'paginated') {\n return `paginated -> ${p.nextUrl ?? '(no href)'}`;\n }\n return `infinite (${p.selector ?? 'sentinel'})`;\n}\n\nfunction renderText(report: ExplainReport): string {\n const lines: string[] = [];\n const root = report.chosenRoot;\n lines.push(\n `readerable: ${report.readerable ? 'yes' : 'no'} parse: ${report.parseSucceeded ? 'ok' : 'fail'} fallback: ${report.fallbackUsed ? 'yes' : 'no'}`,\n );\n lines.push(\n `chosen root: ${root ? `${root.selector} (score ${root.score.toFixed(2)}, ${root.textLength} chars)` : '(no candidate scored)'}`,\n );\n lines.push(`top candidates (${report.candidates.length}):`);\n if (report.candidates.length === 0) {\n lines.push(' (none)');\n }\n report.candidates.forEach((c, i) => {\n lines.push(\n ` ${i + 1}. ${c.selector} score ${c.score.toFixed(2)} (${c.textLength} chars)`,\n );\n });\n const r = report.removedNodes;\n lines.push(\n `removed: total=${r.total} (chrome=${r.chrome}, boilerplate=${r.boilerplate})`,\n );\n lines.push(`gating: ${formatGating(report)}`);\n lines.push(`pagination: ${formatPagination(report)}`);\n lines.push(\n `snapshot (${report.snapshot.html.length} chars${report.snapshot.truncated ? ', truncated' : ''}):`,\n );\n lines.push(report.snapshot.html);\n return lines.join('\\n');\n}\n\nexport function explain(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = explainInputSchema.parse(rawArgs);\n return explainFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\nexport function explainFromHtml(\n input: Readonly<ExplainFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, selectors, topN } = { ...DEFAULTS, ...input };\n const report = buildExplainReport({\n html,\n selectors,\n topN,\n baseUrl,\n });\n const content = renderText(report);\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n chosenRoot: report.chosenRoot,\n candidates: report.candidates,\n readerable: report.readerable,\n parseSucceeded: report.parseSucceeded,\n fallbackUsed: report.fallbackUsed,\n gating: report.gating ?? null,\n pagination: report.pagination ?? null,\n removedNodes: report.removedNodes,\n snapshot: report.snapshot,\n },\n };\n}\n\nexport const EXPLAIN_TOOL_DESCRIPTION = `Post-mortem diagnostics for extraction: shows WHY Readability picked what it picked. Returns the chosen root, the ranked candidate nodes with their REAL Readability contentScore values (read off the DOM expando Readability stamps during scoring), a categorized removed-nodes breakdown, gating/pagination signals, and a snapshot of the normalized HTML fed to Readability. Runs the same normalize + Readability pipeline as \\`extract\\` (no fallback cascade, no Turndown). The server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` (optional) is origin context only.`;\n\nexport function explainHandler(args: unknown): CallToolResult {\n try {\n return explain(args);\n } catch (err) {\n logger.error(\n `explain failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExplainTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'explain',\n {\n title: 'Explain why Readability picked what it picked',\n description: EXPLAIN_TOOL_DESCRIPTION,\n inputSchema: explainInputShape,\n outputSchema: explainOutput,\n },\n explainHandler,\n );\n}\n","// Shared pre-pass for the sibling-cluster detectors (list-detector,\n// grid-detector). Both walk the document for a container whose direct children\n// form a same-shape sibling group; the constants and helpers here are the\n// heuristic-agnostic infrastructure that scan shares. Detector-specific scoring\n// (anchor/pathname signals vs data-cell text) stays in each detector.\n\n// Direct children are scanned only at these container levels. <tbody> covers\n// HN's <table>-based layout; the rest cover semantic (<main>/<article>) and\n// generic (<div>/<section>) wrappers. <table> is included for HTML that skips\n// <tbody>; jsdom synthesizes one anyway, so this is mostly defensive.\nexport const CONTAINER_TAGS = new Set([\n 'ARTICLE',\n 'DIV',\n 'MAIN',\n 'OL',\n 'SECTION',\n 'TABLE',\n 'TBODY',\n 'UL',\n]);\n\n// Subtrees that never carry a page's primary repeated structure. Stripping them\n// is the false-positive guard: without it, an article's <nav> menu or footer\n// link list would look identical to a feed/grid and the detectors would mis-fire\n// on every page with chrome. Distinct from normalize.ts's stripChrome (which\n// removes consent/overlay banners) — this is landmark chrome only.\nconst LANDMARK_CHROME_SELECTOR =\n 'nav, header, footer, aside, [role=\"navigation\"], [role=\"banner\"], ' +\n '[role=\"contentinfo\"], [role=\"complementary\"], [role=\"search\"], ' +\n '[role=\"menu\"], [role=\"menubar\"]';\n\nexport function stripLandmarkChrome(document: Document): void {\n for (const el of document.querySelectorAll(LANDMARK_CHROME_SELECTOR)) {\n el.remove();\n }\n for (const el of document.querySelectorAll('script, style, template')) {\n el.remove();\n }\n}\n\n// Composite of tag + class signature so mixed siblings — e.g. HN's\n// <tr class=\"athing\"> title row vs classless <tr> subtext row, or a grid's\n// header row vs data rows — split into separate candidate groups instead of\n// blending into one ragged group.\nexport function shapeKey(el: Element): string {\n const raw = el.getAttribute('class');\n if (!raw) {\n return el.tagName;\n }\n const normalized = raw.trim().split(/\\s+/).sort().join(' ');\n return normalized ? `${el.tagName}|${normalized}` : el.tagName;\n}\n\n// tag#id.class hint for the winning container. Best-effort, not a unique\n// locator — used only for diagnostics so a human can see which subtree won.\nexport function describeSelector(el: Element): string {\n const parts = [el.tagName.toLowerCase()];\n const id = el.getAttribute('id');\n if (id) {\n parts.push(`#${id}`);\n }\n const cls = el.getAttribute('class');\n if (cls) {\n for (const token of cls.trim().split(/\\s+/)) {\n if (token) {\n parts.push(`.${token}`);\n }\n }\n }\n return parts.join('');\n}\n","import { isElement } from '../pipeline/dom.js';\nimport { resolveCellText } from './cell-text.js';\nimport {\n CONTAINER_TAGS,\n describeSelector,\n shapeKey,\n stripLandmarkChrome,\n} from './sibling-scan.js';\n\nexport interface GridRow {\n readonly cells: readonly string[];\n}\n\nexport type GridConfidence = 'high' | 'low' | 'medium';\n\nexport interface GridDetectionResult {\n readonly colCount: number;\n readonly confidence: GridConfidence;\n readonly containerSelector: string;\n readonly detected: boolean;\n readonly note: string;\n readonly rowCount: number;\n readonly rows: readonly GridRow[];\n readonly rowTag: string;\n}\n\nexport interface DetectGridOptions {\n readonly cellSelector?: string;\n readonly minRows?: number;\n readonly rowSelector?: string;\n}\n\nconst MIN_ROWS = 3;\nconst HIGH_CONF_ROWS = 6;\nconst MIN_CELLS_PER_ROW = 2;\n\nfunction rowCells(row: Element): string[] {\n return Array.from(row.children).map(resolveCellText);\n}\n\nfunction notDetected(note: string): GridDetectionResult {\n return {\n confidence: 'low',\n containerSelector: '',\n detected: false,\n rowCount: 0,\n colCount: 0,\n rows: [],\n rowTag: '',\n note,\n };\n}\n\n// Confidence tracks the detected cluster (the data-row count), not any header\n// row inferred on top of it — a recovered header is inference, not evidence, so\n// it must not push a grid past HIGH_CONF_ROWS. Both detection modes gate on\n// minRows before reaching buildResult, so this is only ever called with a\n// data-row count >= minRows; `low` comes solely from notDetected().\nfunction confidenceFor(dataRowCount: number): GridConfidence {\n return dataRowCount >= HIGH_CONF_ROWS ? 'high' : 'medium';\n}\n\ninterface GridCandidate {\n readonly container: Element;\n readonly containerSelector: string;\n readonly memberCount: number;\n readonly members: readonly Element[];\n readonly rows: readonly string[][];\n readonly rowTag: string;\n readonly totalCellText: number;\n}\n\n// Pad ragged rows to the max cell width so the matrix is rectangular — the\n// GFM/CSV/JSON renderers in policy/tables.ts assume a dense grid (the\n// delimiter row is derived from row-0 width, so a short row would mis-align\n// every column). Mirrors parseTableMatrix's dense-pad step.\n// `evidence` separates the detected cluster from the emitted matrix: rowCount\n// counts every emitted row (data plus any recovered header), but confidence and\n// the \"detected N\" note track the data-row cluster alone, since the header is\n// inferred rather than detected. Omitted in selector mode and when auto-detect\n// recovers no header, where the two counts coincide.\nfunction buildResult(\n raggedRows: readonly string[][],\n rowTag: string,\n containerSelector: string,\n selectorHint?: string,\n evidence?: { readonly dataRowCount: number; readonly headerCount: number },\n): GridDetectionResult {\n let maxCols = 0;\n for (const row of raggedRows) {\n if (row.length > maxCols) {\n maxCols = row.length;\n }\n }\n if (maxCols === 0) {\n return notDetected(\n selectorHint\n ? `not a grid: rows matched ${selectorHint} but no cells were found`\n : 'not a grid: rows matched but no cells were found',\n );\n }\n const rowCount = raggedRows.length;\n const dataRowCount = evidence?.dataRowCount ?? rowCount;\n const headerCount = evidence?.headerCount ?? 0;\n const rows: GridRow[] = raggedRows.map(row => ({\n cells: Array.from({ length: maxCols }, (_, i) => row[i] ?? ''),\n }));\n const where = containerSelector || selectorHint || 'document';\n const descriptor =\n headerCount > 0\n ? `detected ${dataRowCount} ${rowTag} data rows plus ${headerCount} header row${headerCount > 1 ? 's' : ''} (${maxCols} cols) in ${where}`\n : `detected ${dataRowCount} ${rowTag} rows (${maxCols} cols) in ${where}`;\n return {\n confidence: confidenceFor(dataRowCount),\n containerSelector,\n detected: true,\n rowCount,\n colCount: maxCols,\n rows,\n rowTag,\n note: descriptor,\n };\n}\n\nfunction detectSelectorMode(\n document: Document,\n rowSelector: string,\n cellSelector: string,\n minRows: number,\n): GridDetectionResult {\n const rowEls = Array.from(document.querySelectorAll(rowSelector));\n if (rowEls.length < minRows) {\n return notDetected(\n `not a grid: rowSelector \"${rowSelector}\" matched ${rowEls.length} row(s) (min ${minRows})`,\n );\n }\n const rows = rowEls.map(row =>\n Array.from(row.querySelectorAll(cellSelector)).map(resolveCellText),\n );\n return buildResult(rows, rowEls[0].tagName, '', rowSelector);\n}\n\nfunction modalWidth(members: readonly Element[]): number {\n const counts = new Map<number, number>();\n for (const member of members) {\n const width = member.children.length;\n counts.set(width, (counts.get(width) ?? 0) + 1);\n }\n let best = members[0].children.length;\n let bestCount = 0;\n for (const [width, count] of counts) {\n if (count > bestCount) {\n best = width;\n bestCount = count;\n }\n }\n return best;\n}\n\nfunction classTokens(el: Element): Set<string> {\n const cls = el.getAttribute('class');\n return cls ? new Set(cls.trim().split(/\\s+/)) : new Set();\n}\n\n// A div-grid header has no <th>, so recovery leans on two signals, strongest\n// first. (1) An ARIA grid header — role=\"row\" with a columnheader child — is a\n// semantic role and reliable on its own. (2) Class kinship with the data\n// cluster: a real header is the data rows' own class plus a discriminator\n// (\"est-header\"), so it carries every data class token. Chrome such as\n// \"card-header\" / \"page-header\" shares no class with the data rows and must not\n// be promoted — which is why we match token ownership, never the \"header\"\n// substring. Substring matching is what split the header into its own shape\n// group to begin with, and it over-fires on any chrome whose class merely\n// contains the word.\nfunction looksLikeHeader(\n child: Element,\n rowTag: string,\n dataClass: Set<string>,\n dataWidth: number,\n): boolean {\n if (child.tagName !== rowTag || child.children.length !== dataWidth) {\n return false;\n }\n if (\n child.getAttribute('role') === 'row' &&\n Array.from(child.children).some(\n c => c.getAttribute('role') === 'columnheader',\n )\n ) {\n return true;\n }\n const childClass = classTokens(child);\n return (\n dataClass.size > 0 && [...dataClass].every(token => childClass.has(token))\n );\n}\n\n// A header row often carries an extra class token (e.g. \"est-header\") that\n// splits it into its own 1-member shape group, below the minRows cutoff, so the\n// data-row cluster wins on its own and the header is dropped — leaving the\n// first data row mis-read as the header and the real header lost. Recover it:\n// scan the winning container's children that precede the first data row and\n// prepend any sibling that looks like a header for this cluster. Children after\n// the first data row (totals/footer) are left alone.\nfunction collectHeaderRows(\n container: Element,\n members: readonly Element[],\n): string[][] {\n const memberSet = new Set<Element>(members);\n const rowTag = members[0].tagName;\n const dataWidth = modalWidth(members);\n // shapeKey freezes tag + class signature, so every member shares one class\n // set and members[0] represents it; child count is not part of shapeKey, so\n // widths can vary within the group (hence modalWidth above).\n const dataClass = classTokens(members[0]);\n const headers: string[][] = [];\n for (const child of Array.from(container.children)) {\n if (memberSet.has(child)) {\n break;\n }\n if (looksLikeHeader(child, rowTag, dataClass, dataWidth)) {\n headers.push(rowCells(child));\n }\n }\n return headers;\n}\n\nfunction detectAuto(document: Document, minRows: number): GridDetectionResult {\n stripLandmarkChrome(document);\n\n const candidates: GridCandidate[] = [];\n for (const container of document.querySelectorAll('*')) {\n if (!CONTAINER_TAGS.has(container.tagName)) {\n continue;\n }\n // Group direct element-children by shape so a homogeneous row cluster\n // surfaces as one candidate and mixed-shape siblings (header row vs data\n // rows) split apart rather than blending into a ragged group.\n const groups = new Map<string, Element[]>();\n for (const child of Array.from(container.childNodes)) {\n if (!isElement(child)) {\n continue;\n }\n const key = shapeKey(child);\n const bucket = groups.get(key);\n if (bucket) {\n bucket.push(child);\n } else {\n groups.set(key, [child]);\n }\n }\n for (const members of groups.values()) {\n if (members.length < minRows) {\n continue;\n }\n // A row needs at least two cells to be a row; a group of single-child\n // wrappers is a list of links/labels, not a data grid.\n if (!members.every(m => m.children.length >= MIN_CELLS_PER_ROW)) {\n continue;\n }\n const rows = members.map(rowCells);\n const totalCellText = rows.reduce(\n (sum, row) => sum + row.reduce((s, c) => s + c.length, 0),\n 0,\n );\n candidates.push({\n container,\n containerSelector: describeSelector(container),\n members,\n rows,\n rowTag: members[0].tagName,\n memberCount: members.length,\n totalCellText,\n });\n }\n }\n\n if (candidates.length === 0) {\n return notDetected(\n 'not a grid: no repeating row structure (≥3 same-shape siblings each with ≥2 direct element-children, outside nav/header/footer/aside)',\n );\n }\n\n // Most rows wins; ties break on total cell text so a sparse 6-row grid beats\n // a dense 6-row one only when it carries more substance, and a longer\n // analyst-estimates table beats a stub sidebar mini-grid on both axes.\n let winner = candidates[0];\n for (let i = 1; i < candidates.length; i++) {\n const candidate = candidates[i];\n if (\n candidate.memberCount > winner.memberCount ||\n (candidate.memberCount === winner.memberCount &&\n candidate.totalCellText > winner.totalCellText)\n ) {\n winner = candidate;\n }\n }\n const headerRows = collectHeaderRows(winner.container, winner.members);\n return buildResult(\n [...headerRows, ...winner.rows],\n winner.rowTag,\n winner.containerSelector,\n undefined,\n { dataRowCount: winner.memberCount, headerCount: headerRows.length },\n );\n}\n\n// Detect a CSS-grid/div \"table\" — the div equivalent of extract_tables. In\n// selector mode (both rowSelector and cellSelector given) the rows and cells\n// are whatever the caller names. In auto mode, chrome is stripped first and\n// the container whose direct children form the largest same-shape sibling\n// group (each member a row of ≥2 direct element-children) wins. The resulting\n// rows are padded to a dense rectangular matrix so the shared table renderer\n// can serialize them as gfm/csv/json.\nexport function detectGrid(\n document: Document,\n opts?: DetectGridOptions,\n): GridDetectionResult {\n const minRows = opts?.minRows ?? MIN_ROWS;\n if (opts?.rowSelector && opts.cellSelector) {\n return detectSelectorMode(\n document,\n opts.rowSelector,\n opts.cellSelector,\n minRows,\n );\n }\n return detectAuto(document, minRows);\n}\n","import type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { applySelectors } from '../pipeline/normalize.js';\nimport { detectGrid } from '../policy/grid-detector.js';\nimport { renderTable } from '../policy/tables.js';\nimport { readHtmlFile } from './html-source.js';\nimport { extractGridOutputShape } from './output-schema.js';\nimport {\n type ExtractGridFromHtmlInput,\n type ExtractGridInput,\n extractGridInputSchema,\n extractGridInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\ninterface GridEntry {\n readonly cols: number;\n readonly markdown: string;\n readonly rows: number;\n}\n\nconst NO_GRID = '(no repeating grid found)';\n\nexport function extractGrid(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = extractGridInputSchema.parse(rawArgs);\n return extractGridFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\n// Schema defaults for callers that pass only a subset of the knobs (format).\nconst DEFAULTS: Omit<ExtractGridInput, 'localPath'> =\n extractGridInputSchema.parse({ localPath: '' });\n\nexport function extractGridFromHtml(\n input: Readonly<ExtractGridFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, format, selectors, rowSelector, cellSelector } = {\n ...DEFAULTS,\n ...input,\n };\n\n // Same raw-DOM rationale as extract_tables/extract_list: the repeating grid\n // lives in statically-rendered divs that Readability/normalize would discard\n // (nav/aside/boilerplate), and grid shape is unaffected by unsanitized\n // scripts/styles. Chrome stripping lives inside detectGrid (auto mode only)\n // so selector mode sees the page as captured.\n const { document } = buildDocument(html, baseUrl);\n applySelectors(document, selectors);\n const result = detectGrid(document, { rowSelector, cellSelector });\n\n let content = NO_GRID;\n let grid: GridEntry = { rows: 0, cols: 0, markdown: '' };\n if (result.detected && result.rows.length > 0) {\n const matrix = result.rows.map(row => [...row.cells]);\n const markdown = renderTable(matrix, format);\n content = markdown;\n grid = { rows: result.rowCount, cols: result.colCount, markdown };\n }\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n grid,\n diagnostics: {\n confidence: result.confidence,\n containerSelector: result.containerSelector,\n detected: result.detected,\n rowCount: result.rowCount,\n colCount: result.colCount,\n rowTag: result.rowTag,\n note: result.note,\n },\n metadata: { baseUrl, format, detected: result.detected },\n },\n };\n}\n\nexport const EXTRACT_GRID_TOOL_DESCRIPTION = `Detect and extract a CSS-grid / div \"table\" from already-rendered (post-JavaScript) HTML — the div equivalent of \\`extract_tables\\` for SPAs that render data into repeating \\`<div>\\` rows instead of \\`<table>\\`. Supports auto-detect (find the container whose direct children form the largest same-shape sibling group of ≥3 rows, each row a set of ≥2 direct element-children) and explicit \\`rowSelector\\` + \\`cellSelector\\` selector mode (cells scoped to each row subtree). Renders the matrix through the SAME gfm/csv/json renderer as \\`extract_tables\\`. Runs no Readability, no Turndown, no sanitization — the server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` (optional) is origin context only.`;\n\nexport function extractGridHandler(args: unknown): CallToolResult {\n try {\n return extractGrid(args);\n } catch (err) {\n logger.error(\n `extract_grid failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExtractGridTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'extract_grid',\n {\n title:\n 'Extract a CSS-grid / div table (the div equivalent of extract_tables)',\n description: EXTRACT_GRID_TOOL_DESCRIPTION,\n inputSchema: extractGridInputShape,\n outputSchema: extractGridOutputShape,\n },\n extractGridHandler,\n );\n}\n","import type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { applySelectors } from '../pipeline/normalize.js';\nimport { absolutize } from '../pipeline/urls.js';\nimport { readHtmlFile } from './html-source.js';\nimport { extractLinksOutputShape } from './output-schema.js';\nimport {\n type ExtractLinksFromHtmlInput,\n type ExtractLinksInput,\n extractLinksInputSchema,\n extractLinksInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nexport interface ExtractedLink {\n readonly href: string;\n readonly isExternal: boolean;\n readonly rel: string;\n readonly text: string;\n}\n\nconst MAX_TEXT_LENGTH = 300;\n\nfunction clipText(raw: string): string {\n const collapsed = raw.replace(/\\s+/g, ' ').trim();\n return collapsed.length > MAX_TEXT_LENGTH\n ? `${collapsed.slice(0, MAX_TEXT_LENGTH)}…`\n : collapsed;\n}\n\n// Non-http(s) schemes (mailto/tel/javascript/data) have an opaque origin and\n// would otherwise compare as \"different\" — treat them as non-external so the\n// isExternal flag tracks real cross-origin navigation only.\nfunction isWebOrigin(parsed: URL): boolean {\n return parsed.protocol === 'http:' || parsed.protocol === 'https:';\n}\n\nfunction resolveExternal(absolutizedHref: string, baseUrl: string): boolean {\n let parsedHref: URL;\n try {\n parsedHref = new URL(absolutizedHref);\n } catch {\n return false;\n }\n if (!isWebOrigin(parsedHref)) {\n return false;\n }\n try {\n return new URL(baseUrl).origin !== parsedHref.origin;\n } catch {\n return false;\n }\n}\n\n// Drop <script>/<template> for safety (templates never render, scripts carry no\n// crawl value), but keep nav/footer/main — crawl-relevant links live there.\nfunction pruneUnsafeRoots(document: Document): void {\n for (const el of document.querySelectorAll('script, template')) {\n el.remove();\n }\n}\n\nexport function extractLinks(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = extractLinksInputSchema.parse(rawArgs);\n return extractLinksFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\n// Schema defaults for callers that pass only a subset of the knobs (sameOriginOnly).\nconst DEFAULTS: Omit<ExtractLinksInput, 'localPath'> =\n extractLinksInputSchema.parse({ localPath: '' });\n\nexport function extractLinksFromHtml(\n input: Readonly<ExtractLinksFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, sameOriginOnly, selectors } = {\n ...DEFAULTS,\n ...input,\n };\n\n const { document } = buildDocument(html, baseUrl);\n applySelectors(document, selectors);\n pruneUnsafeRoots(document);\n\n const links: ExtractedLink[] = [];\n for (const anchor of document.querySelectorAll('a')) {\n const rawHref = anchor.getAttribute('href');\n if (!rawHref) {\n continue;\n }\n const href = absolutize(rawHref, baseUrl);\n const isExternal = baseUrl ? resolveExternal(href, baseUrl) : false;\n if (sameOriginOnly && isExternal) {\n continue;\n }\n links.push({\n text: clipText(anchor.textContent),\n href,\n rel: anchor.getAttribute('rel') ?? '',\n isExternal,\n });\n }\n\n const content = renderLinksIndex(links);\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n links,\n metadata: { baseUrl },\n },\n };\n}\n\n// One `- [text](href)` line per link; never blank so content[0].text is scannable.\nfunction renderLinksIndex(links: readonly ExtractedLink[]): string {\n if (links.length === 0) {\n return '(no links found)';\n }\n return links\n .map(link => `- [${link.text || '(no text)'}](${link.href})`)\n .join('\\n');\n}\n\nexport const EXTRACT_LINKS_TOOL_DESCRIPTION = `Return a structured list of anchor links from already-rendered (post-JavaScript) HTML — \\`[{text, href, rel, isExternal}]\\` in document order, hrefs absolutized against \\`baseUrl\\`. No Readability scoring, Turndown, or sanitization — links are gathered from the raw parsed DOM so nav/footer/main links survive. Pairs with chrome-devtools for crawl/navigation decisions. The server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` is origin context only (never fetched).`;\n\nexport function extractLinksHandler(args: unknown): CallToolResult {\n try {\n return extractLinks(args);\n } catch (err) {\n logger.error(\n `extract_links failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExtractLinksTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'extract_links',\n {\n title: 'Extract anchor links',\n description: EXTRACT_LINKS_TOOL_DESCRIPTION,\n inputSchema: extractLinksInputShape,\n outputSchema: extractLinksOutputShape,\n },\n extractLinksHandler,\n );\n}\n","import { isElement } from '../pipeline/dom.js';\nimport { absolutize } from '../pipeline/urls.js';\nimport {\n CONTAINER_TAGS,\n describeSelector,\n shapeKey,\n stripLandmarkChrome,\n} from './sibling-scan.js';\n\nexport interface ListItem {\n readonly score: number;\n readonly snippet: string;\n readonly title: string;\n readonly url: string;\n}\n\nexport type ListConfidence = 'high' | 'low' | 'medium';\n\nexport interface ListDetectionResult {\n readonly confidence: ListConfidence;\n readonly containerSelector: string;\n readonly detected: boolean;\n readonly itemCount: number;\n readonly items: readonly ListItem[];\n readonly itemTag: string;\n readonly note: string;\n}\n\n// Three siblings of the same shape (tag + class signature), each carrying a\n// real navigation anchor, is the smallest cluster that is not accidentally a\n// nav menu or a article's <ul> of inline links. Below this the detector\n// refuses to claim the page is a list.\nconst MIN_ITEMS = 3;\n\n// Six-or-more items with non-trivial body text per item is the empirical\n// signature of a true index/search/blog-roll page (HN shows 30, Google ~10,\n// WP indexes ~10). Used to separate `high` from `medium` confidence so a\n// 3-item related-links block doesn't masquerade as a feed.\nconst HIGH_CONF_ITEMS = 6;\nconst HIGH_CONF_AVG_SCORE = 30;\n\nconst MAX_SNIPPET_CHARS = 200;\n\n// href schemes that don't point at a list target. Excluding them keeps mailto:\n// /tel:/javascript:/ anchors from satisfying the \"every item has a link\" bar.\nfunction isNavigationHref(href: string): boolean {\n if (!href || href === '#') {\n return false;\n }\n const lower = href.toLowerCase();\n return (\n !lower.startsWith('javascript:') &&\n !lower.startsWith('mailto:') &&\n !lower.startsWith('tel:')\n );\n}\n\nfunction anchorText(anchor: HTMLAnchorElement): string {\n return anchor.textContent.replace(/\\s+/g, ' ').trim();\n}\n\n// Anchors with a navigation-worthy href AND visible text. Lives inside the\n// candidate child so a single <a href=\"#\"> footer link doesn't satisfy the\n// \"every item has a link\" requirement.\nfunction navigationAnchors(child: Element): HTMLAnchorElement[] {\n const anchors: HTMLAnchorElement[] = [];\n for (const el of child.querySelectorAll('a[href]')) {\n const anchor = el as HTMLAnchorElement;\n const href = anchor.getAttribute('href') ?? '';\n if (!isNavigationHref(href)) {\n continue;\n }\n if (anchorText(anchor).length === 0) {\n continue;\n }\n anchors.push(anchor);\n }\n return anchors;\n}\n\nfunction pickPrimaryAnchor(\n anchors: readonly HTMLAnchorElement[],\n): HTMLAnchorElement {\n let primary = anchors[0];\n let best = anchorText(primary).length;\n for (let i = 1; i < anchors.length; i++) {\n const len = anchorText(anchors[i]).length;\n if (len > best) {\n primary = anchors[i];\n best = len;\n }\n }\n return primary;\n}\n\nfunction clipSnippet(raw: string): string {\n const collapsed = raw.replace(/\\s+/g, ' ').trim();\n if (collapsed.length <= MAX_SNIPPET_CHARS) {\n return collapsed;\n }\n return `${collapsed.slice(0, MAX_SNIPPET_CHARS)}…`;\n}\n\n// Score = primary-anchor text length + non-link body text length. A pure\n// link-density penalty (textLength * (1 - linkDensity)) ranks HN's subtext\n// rows ABOVE the title rows because the title row is anchor-dominated (its\n// title IS the link), so non-link body alone mis-ranks. Adding the primary\n// anchor's text length rewards long titles (real feed items) over short nav\n// labels (\"Home\", \"Sign in\"), which is the signal we actually want for both\n// the candidate tiebreak and the high-confidence threshold.\nfunction scoreItem(\n textLength: number,\n linkTextLength: number,\n primaryAnchorTextLength: number,\n): number {\n if (textLength === 0) {\n return 0;\n }\n const nonLinkBody = Math.max(0, textLength - linkTextLength);\n return primaryAnchorTextLength + nonLinkBody;\n}\n\nfunction extractItem(\n child: Element,\n baseUrl: string | undefined,\n): ListItem | null {\n const anchors = navigationAnchors(child);\n if (anchors.length === 0) {\n return null;\n }\n const primary = pickPrimaryAnchor(anchors);\n const title = anchorText(primary);\n if (!title) {\n return null;\n }\n const href = primary.getAttribute('href') ?? '';\n const url = absolutize(href, baseUrl);\n if (!url) {\n return null;\n }\n\n const fullText = child.textContent.replace(/\\s+/g, ' ').trim();\n // Snippet = body text with the title peeled off the front when present, so\n // the title link doesn't echo into the excerpt.\n const snippet =\n fullText === title\n ? ''\n : fullText.startsWith(title)\n ? clipSnippet(fullText.slice(title.length + 1))\n : clipSnippet(fullText);\n\n const linkTextLength = anchors.reduce(\n (sum, anchor) => sum + anchorText(anchor).length,\n 0,\n );\n return {\n score: scoreItem(fullText.length, linkTextLength, title.length),\n snippet,\n title,\n url,\n };\n}\n\ninterface Candidate {\n readonly containerSelector: string;\n readonly distinctPathnames: number;\n readonly items: readonly ListItem[];\n readonly itemTag: string;\n readonly totalScore: number;\n}\n\n// Count of distinct URL pathnames a candidate's items point at. This is the\n// signal that separates a real feed from a metadata cluster: a feed's items\n// each navigate to a distinct destination (HN title rows link to ~30 different\n// external stories; a blog index links to /post-1../post-N; a search results\n// page links to a heterogeneous set of sites), so distinctPathnames ≈ item\n// count. A metadata cluster's items all navigate to the same internal route —\n// HN's subtext rows are 31 anchors all pointing at news.ycombinator.com/item?id=…,\n// collapsing to pathname=/item (distinctPathnames = 1). Counting items alone\n// lets subtext win on HN (31 vs 30 title rows) because of the trailing \"More\"\n// row; counting distinct destinations restores the title group as the winner.\nfunction distinctPathnames(items: readonly ListItem[]): number {\n const paths = new Set<string>();\n for (const item of items) {\n try {\n paths.add(new URL(item.url).pathname);\n } catch {\n paths.add(item.url);\n }\n }\n return paths.size;\n}\n\nfunction collectCandidates(document: Document, baseUrl: string | undefined) {\n const candidates: Candidate[] = [];\n for (const container of document.querySelectorAll('*')) {\n if (!CONTAINER_TAGS.has(container.tagName)) {\n continue;\n }\n // Group direct element-children by shape so homogeneous sibling lists\n // surface as one cluster and mixed-shape siblings (e.g. HN's athing +\n // subtext rows) split apart.\n const groups = new Map<string, Element[]>();\n for (const child of Array.from(container.childNodes)) {\n if (!isElement(child)) {\n continue;\n }\n const key = shapeKey(child);\n const bucket = groups.get(key);\n if (bucket) {\n bucket.push(child);\n } else {\n groups.set(key, [child]);\n }\n }\n for (const children of groups.values()) {\n if (children.length < MIN_ITEMS) {\n continue;\n }\n // EVERY member must carry a real anchor — one linkless <li> breaks\n // homogeneity and rejects the group, which is what keeps a <ul> of\n // plain-text list items (e.g. recipe ingredients) from triggering.\n if (!children.every(child => navigationAnchors(child).length > 0)) {\n continue;\n }\n const items: ListItem[] = [];\n for (const child of children) {\n const item = extractItem(child, baseUrl);\n if (item) {\n items.push(item);\n }\n }\n if (items.length < MIN_ITEMS) {\n continue;\n }\n candidates.push({\n containerSelector: describeSelector(container),\n distinctPathnames: distinctPathnames(items),\n itemTag: children[0].tagName,\n items,\n totalScore: items.reduce((sum, item) => sum + item.score, 0),\n });\n }\n }\n return candidates;\n}\n\nfunction confidenceFor(items: readonly ListItem[]): ListConfidence {\n if (items.length < MIN_ITEMS) {\n return 'low';\n }\n if (items.length >= HIGH_CONF_ITEMS) {\n const avg = items.reduce((sum, item) => sum + item.score, 0) / items.length;\n return avg >= HIGH_CONF_AVG_SCORE ? 'high' : 'medium';\n }\n return 'medium';\n}\n\nfunction notDetected(note: string): ListDetectionResult {\n return {\n confidence: 'low',\n containerSelector: '',\n detected: false,\n itemCount: 0,\n itemTag: '',\n items: [],\n note,\n };\n}\n\n// Detect a list/feed/index structure on a non-article page. Strips chrome\n// (nav/header/footer/aside + ARIA roles) first so an article's menu doesn't\n// look like a 4-item feed, then walks for containers whose direct children\n// form a homogeneous sibling group of ≥3 elements each carrying a real anchor.\n// Winner selection prefers the candidate whose items navigate to the most\n// distinct destinations — the signature of a real feed — over a same-path\n// metadata cluster (HN's subtext rows all link to /item?id=…). Item count is\n// the second key (a 30-item feed beats a 4-item related-links block) and total\n// per-item score breaks remaining ties.\nexport function detectList(\n document: Document,\n baseUrl?: string,\n): ListDetectionResult {\n stripLandmarkChrome(document);\n\n const candidates = collectCandidates(document, baseUrl);\n if (candidates.length === 0) {\n return notDetected(\n 'not a list: no repeated item structure with links (≥3 same-shape siblings each carrying an anchor, outside nav/header/footer/aside)',\n );\n }\n\n let winner = candidates[0];\n for (let i = 1; i < candidates.length; i++) {\n const candidate = candidates[i];\n if (\n candidate.distinctPathnames > winner.distinctPathnames ||\n (candidate.distinctPathnames === winner.distinctPathnames &&\n candidate.items.length > winner.items.length) ||\n (candidate.distinctPathnames === winner.distinctPathnames &&\n candidate.items.length === winner.items.length &&\n candidate.totalScore > winner.totalScore)\n ) {\n winner = candidate;\n }\n }\n\n if (winner.items.length < MIN_ITEMS) {\n return notDetected(\n 'not a list: best candidate had fewer than 3 extracted items',\n );\n }\n\n const items = winner.items;\n return {\n confidence: confidenceFor(items),\n containerSelector: winner.containerSelector,\n detected: true,\n itemCount: items.length,\n itemTag: winner.itemTag,\n items,\n note: `detected ${items.length} ${winner.itemTag} items in ${winner.containerSelector}`,\n };\n}\n","import type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { applySelectors } from '../pipeline/normalize.js';\nimport {\n detectList,\n type ListDetectionResult,\n type ListItem,\n} from '../policy/list-detector.js';\nimport { readHtmlFile } from './html-source.js';\nimport { extractListOutputShape } from './output-schema.js';\nimport {\n type ExtractListFromHtmlInput,\n extractListInputSchema,\n extractListInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst NOT_A_LIST = 'not a list: no repeated item structure with links';\n\nfunction renderItems(result: ListDetectionResult): string {\n if (!result.detected || result.items.length === 0) {\n return result.note || NOT_A_LIST;\n }\n return result.items\n .map((item, index) => {\n const head = `${index + 1}. ${item.title} — ${item.url}`;\n return item.snippet ? `${head}\\n ${item.snippet}` : head;\n })\n .join('\\n');\n}\n\nfunction toStructuredItem(item: ListItem) {\n return {\n score: item.score,\n snippet: item.snippet,\n title: item.title,\n url: item.url,\n };\n}\n\nexport function extractList(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = extractListInputSchema.parse(rawArgs);\n return extractListFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\nexport function extractListFromHtml(\n input: Readonly<ExtractListFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, selectors } = input;\n\n // No Readability/Turndown/normalize: list pages survive on raw DOM shape\n // (sibling TR/LI/ARTICLE clusters), and the article normalizer would\n // discard the very chrome-bearing structure the detector scores against.\n // Chrome stripping (nav/header/footer/aside) lives inside detectList so\n // this tool sees the page as captured.\n const { document } = buildDocument(html, baseUrl);\n applySelectors(document, selectors);\n const result = detectList(document, baseUrl);\n const content = renderItems(result);\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n items: result.items.map(toStructuredItem),\n diagnostics: {\n confidence: result.confidence,\n containerSelector: result.containerSelector,\n detected: result.detected,\n itemCount: result.itemCount,\n itemTag: result.itemTag,\n note: result.note,\n },\n metadata: { baseUrl },\n },\n };\n}\n\nexport const EXTRACT_LIST_TOOL_DESCRIPTION = `Detect and extract a list/feed/index structure from already-rendered (post-JavaScript) HTML — for HN-style, search-result, and blog-index pages that Readability cannot turn into one article. Returns \\`{items: [{title, url, snippet, score}], diagnostics}\\` instead of one article. 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, and the cluster with the most items wins. No Readability, no Turndown, no sanitization. The server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` (optional) is origin context for absolutizing item hrefs.`;\n\nexport function extractListHandler(args: unknown): CallToolResult {\n try {\n return extractList(args);\n } catch (err) {\n logger.error(\n `extract_list failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExtractListTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'extract_list',\n {\n title: 'Extract a list/feed (HN/search/blog-index pages)',\n description: EXTRACT_LIST_TOOL_DESCRIPTION,\n inputSchema: extractListInputShape,\n outputSchema: extractListOutputShape,\n },\n extractListHandler,\n );\n}\n","import type { Metadata } from '../pipeline/context.js';\nimport type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { normalizeDocument } from '../pipeline/normalize.js';\nimport { resolveMetadata } from '../policy/metadata.js';\nimport { readHtmlFile } from './html-source.js';\nimport { extractMetadataOutputShape } from './output-schema.js';\nimport {\n type ExtractMetadataFromHtmlInput,\n extractMetadataInputSchema,\n extractMetadataInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n// Bibliographic fields only — wordCount/readingTimeMin/tokenEstimate are\n// meaningless without the extracted body, so a metadata-only caller never\n// sees wordCount: 0.\nconst BIBLIOGRAPHIC_KEYS = [\n 'title',\n 'byline',\n 'siteName',\n 'lang',\n 'publishedTime',\n 'excerpt',\n 'canonical',\n 'baseUrl',\n] as const satisfies readonly (keyof Metadata)[];\n\nfunction pickBibliographic(\n metadata: Readonly<Metadata>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n for (const key of BIBLIOGRAPHIC_KEYS) {\n const value = metadata[key];\n if (value !== undefined) {\n out[key] = value;\n }\n }\n return out;\n}\n\n// One line per present field; never blank so content[0].text is always scannable.\nfunction renderMetadataLines(\n metadata: Readonly<Record<string, string>>,\n): string {\n const lines = Object.entries(metadata).map(\n ([key, value]) => `${key}: ${value}`,\n );\n return lines.length > 0 ? lines.join('\\n') : '(no metadata found)';\n}\n\nexport function extractMetadataDocument(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = extractMetadataInputSchema.parse(rawArgs);\n return extractMetadataDocumentFromHtml({\n html: readHtmlFile(localPath),\n ...rest,\n });\n}\n\nexport function extractMetadataDocumentFromHtml(\n input: Readonly<ExtractMetadataFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl } = input;\n\n const { document } = buildDocument(html, baseUrl);\n normalizeDocument(document);\n const resolved = resolveMetadata({\n document,\n textContent: '',\n wordCount: 0,\n readingTimeMin: 0,\n baseUrl,\n });\n\n const metadata = pickBibliographic(resolved);\n const content = renderMetadataLines(metadata);\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n metadata,\n },\n };\n}\n\nexport const EXTRACT_METADATA_TOOL_DESCRIPTION = `Return only the bibliographic metadata (title, byline, siteName, lang, publishedTime, excerpt, canonical, baseUrl) of already-rendered (post-JavaScript) HTML without running Readability/Turndown — a fast pre-check for crawlers and citation. Resolves the same metadata cascade as \\`extract\\` (JSON-LD → OpenGraph → Twitter → <meta> → <time> → <title>), plus <link rel=\"canonical\"> → og:url. The server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` is origin context only (never fetched).`;\n\nexport function extractMetadataHandler(args: unknown): CallToolResult {\n try {\n return extractMetadataDocument(args);\n } catch (err) {\n logger.error(\n `extract_metadata failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExtractMetadataTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'extract_metadata',\n {\n title: 'Extract metadata only (no Readability)',\n description: EXTRACT_METADATA_TOOL_DESCRIPTION,\n inputSchema: extractMetadataInputShape,\n outputSchema: extractMetadataOutputShape,\n },\n extractMetadataHandler,\n );\n}\n","export interface OutlineEntry {\n readonly anchor: string;\n readonly level: number;\n readonly text: string;\n}\n\n// Collapse whitespace, trim, then strip leading/trailing `#` (GitHub permalinks\n// append `<a>#</a>`, so heading textContent ends with `#`).\nexport function normalizeHeadingText(raw: string): string {\n const collapsed = raw.replace(/\\s+/g, ' ').trim();\n return collapsed.replace(/^#+|#+$/g, '').trim();\n}\n\n// GFM anchor algorithm (comrak `anchorize`): keep Unicode letters, marks,\n// numbers, and connector punctuation; map spaces to hyphens. No hyphen collapse\n// or trim, so anchors match GitHub and non-Latin scripts survive. An all-symbol\n// heading yields an empty slug; fall back to `section` so anchors stay non-empty.\nfunction slugify(text: string): string {\n const cleaned = text\n .toLowerCase()\n .replace(/[^\\p{L}\\p{M}\\p{N}\\p{Pc} -]/gu, '');\n return cleaned.replace(/ /g, '-') || 'section';\n}\n\n// `href=\"#\"` (empty fragment) does not win — fall through to slugify.\nfunction linkAnchor(heading: Element): string | undefined {\n for (const link of heading.querySelectorAll('a[href^=\"#\"]')) {\n const href = link.getAttribute('href');\n if (!href) {\n continue;\n }\n const fragment = href.slice(1);\n if (fragment) {\n return fragment;\n }\n }\n return undefined;\n}\n\nfunction resolveCandidate(\n heading: Element,\n text: string,\n): { readonly anchor: string; readonly explicit: boolean } {\n const id = heading.getAttribute('id');\n if (id) {\n return { anchor: id, explicit: true };\n }\n const link = linkAnchor(heading);\n if (link) {\n return { anchor: link, explicit: true };\n }\n return { anchor: slugify(text), explicit: false };\n}\n\n// Explicit anchors are kept verbatim; generated slugs are collision-suffixed\n// (`-1`, `-2`, …), first occurrence bare.\nfunction dedupe(\n candidate: string,\n explicit: boolean,\n used: Set<string>,\n): string {\n if (explicit || !used.has(candidate)) {\n return candidate;\n }\n let suffix = 1;\n while (used.has(`${candidate}-${suffix}`)) {\n suffix++;\n }\n return `${candidate}-${suffix}`;\n}\n\nexport function resolveOutline(document: Document): OutlineEntry[] {\n const entries: OutlineEntry[] = [];\n const used = new Set<string>();\n const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');\n headings.forEach(heading => {\n const rawText = heading.textContent;\n if (!rawText.trim()) {\n return;\n }\n const text = normalizeHeadingText(rawText);\n const level = Number.parseInt(heading.tagName.slice(1), 10);\n const { anchor: candidate, explicit } = resolveCandidate(heading, text);\n const anchor = dedupe(candidate, explicit, used);\n used.add(anchor);\n entries.push({ anchor, level, text });\n });\n return entries;\n}\n","import { isElement } from '../pipeline/dom.js';\nimport { normalizeHeadingText } from './outline.js';\n\ninterface HeadingMatch {\n readonly heading: Element;\n readonly level: number;\n}\n\n// Match the first heading by normalized text. Exact equality wins; otherwise\n// fall back to the first substring match so a short query (\"auth\") still\n// lands on a longer heading (\"Authentication\") without forcing an exact match.\nfunction findHeading(\n document: Document,\n query: string,\n): HeadingMatch | undefined {\n const needle = normalizeHeadingText(query).toLowerCase();\n if (!needle) {\n return undefined;\n }\n const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');\n let substring: HeadingMatch | undefined;\n for (const heading of headings) {\n const text = normalizeHeadingText(heading.textContent).toLowerCase();\n if (!text) {\n continue;\n }\n const level = Number.parseInt(heading.tagName.slice(1), 10);\n if (text === needle) {\n return { heading, level };\n }\n if (!substring && text.includes(needle)) {\n substring = { heading, level };\n }\n }\n return substring;\n}\n\n// Elements whose direct children form the section flow. The matched heading's\n// nearest such ancestor is the level at which the section's content lives as\n// siblings: on docs sites (GitHub markdown, etc.) the heading is wrapped in\n// <div class=\"markdown-heading\">, so the body <p> is a sibling of the wrapper\n// div, not of the <h2>. Walking the <h2>'s own siblings would capture only the\n// permalink anchor and miss the body.\nconst FLOW_CONTAINERS = new Set([\n 'ARTICLE',\n 'ASIDE',\n 'BLOCKQUOTE',\n 'BODY',\n 'DD',\n 'DETAILS',\n 'DT',\n 'FOOTER',\n 'HEADER',\n 'LI',\n 'MAIN',\n 'NAV',\n 'SECTION',\n 'TD',\n]);\n\nfunction findFlowContainer(heading: Element): Element {\n let ancestor: Element | null = heading.parentElement;\n while (ancestor) {\n if (FLOW_CONTAINERS.has(ancestor.tagName)) {\n return ancestor;\n }\n ancestor = ancestor.parentElement;\n }\n // BODY is in FLOW_CONTAINERS, so the loop reaches it for any attached\n // heading; this fallback only covers a detached heading.\n return heading.ownerDocument.body;\n}\n\n// Level of the first heading at or inside `el`, or undefined when `el` is\n// neither a heading nor a wrapper around one. Used to test whether a following\n// sibling terminates the section.\nfunction firstHeadingLevel(el: Element): number | undefined {\n const direct = /^H([1-6])$/.exec(el.tagName);\n if (direct) {\n return Number.parseInt(direct[1], 10);\n }\n const inner = el.querySelector('h1, h2, h3, h4, h5, h6');\n if (inner) {\n return Number.parseInt(inner.tagName.slice(1), 10);\n }\n return undefined;\n}\n\n// Wraps the matched heading and its subtree in a `<section data-rdrm-section-scope>`\n// so the existing selectors.include path can isolate it. Returns false (and\n// leaves the document untouched) when no heading matches.\nexport function scopeToHeading(\n document: Document,\n headingText: string,\n): boolean {\n const match = findHeading(document, headingText);\n if (!match) {\n return false;\n }\n const { heading, level } = match;\n const container = findFlowContainer(heading);\n let startChild: Element = heading;\n while (startChild.parentElement && startChild.parentElement !== container) {\n startChild = startChild.parentElement;\n }\n const wrap = document.createElement('section');\n wrap.setAttribute('data-rdrm-section-scope', '');\n container.insertBefore(wrap, startChild);\n let node: Node | null = startChild;\n while (node) {\n const next: Node | null = node.nextSibling;\n wrap.appendChild(node);\n // A section ends at the next same-or-higher-level heading (level <= L):\n // deeper headings (h3 under an h2) belong to this section, peers and\n // shallower headings start the next one. The terminating sibling may be a\n // wrapper around the heading (e.g. <div class=\"markdown-heading\">), so the\n // check looks for a heading at OR inside the sibling.\n if (next !== null && isElement(next)) {\n const nextLevel = firstHeadingLevel(next);\n if (nextLevel !== undefined && nextLevel <= level) {\n break;\n }\n }\n node = next;\n }\n return true;\n}\n","import type { ToolHandle } from '../server.js';\n\nimport { ExtractionError, toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { normalizeDocument } from '../pipeline/normalize.js';\nimport { scopeToHeading } from '../policy/section.js';\nimport { extractArticleFromHtml } from './extract.js';\nimport { readHtmlFile } from './html-source.js';\nimport { outputSchemaShape } from './output-schema.js';\nimport {\n type ExtractSectionFromHtmlInput,\n extractSectionInputSchema,\n extractSectionInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst SECTION_SCOPE_SELECTOR = '[data-rdrm-section-scope]';\n\nexport function extractSection(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = extractSectionInputSchema.parse(rawArgs);\n return extractSectionFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\nexport function extractSectionFromHtml(\n input: Readonly<ExtractSectionFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, selector, heading } = input;\n\n if (selector !== undefined) {\n return extractArticleFromHtml({\n html,\n baseUrl,\n selectors: { include: selector },\n });\n }\n // The superRefine on the schema enforces selector/heading XOR, so reaching\n // here means heading is set — but its declared type is still optional, so\n // narrow explicitly rather than asserting.\n if (heading === undefined) {\n throw new ExtractionError(\n 'Provide exactly one of `selector` or `heading`.',\n );\n }\n\n // Heading mode: wrap the matched subtree, re-serialize, and route through\n // extractArticleFromHtml's selectors.include so this tool stays a thin\n // resolver over the existing extraction pipeline — no forked extraction logic.\n // The DOM is parsed+normalized twice: here to scope against a normalized\n // DOM, and again inside the extract worker — which owns detectGating-before-\n // normalize, resolveLazyImages, and applySelectors. Folding scoping into\n // that pipeline would breach its invariants for a non-hot path; accepted.\n const { document } = buildDocument(html, baseUrl);\n normalizeDocument(document);\n if (!scopeToHeading(document, heading)) {\n throw new ExtractionError(`no heading matched: ${heading}`);\n }\n const scoped = `<!DOCTYPE html><html><head></head><body>${document.body.innerHTML}</body></html>`;\n return extractArticleFromHtml({\n html: scoped,\n baseUrl,\n selectors: { include: SECTION_SCOPE_SELECTOR },\n });\n}\n\nexport const EXTRACT_SECTION_TOOL_DESCRIPTION = `Extract one section of an already-rendered (post-JavaScript) HTML document and return its Markdown + metadata + diagnostics — a thin resolver over extract’s \\`selectors.include\\` path, not a new extractor. Pick the section by CSS \\`selector\\` (passed straight through) OR by \\`heading\\` text (case-insensitive, first match wins; the section spans from the matched heading to the next same-or-higher-level heading). Exactly one of \\`selector\\`/\\`heading\\` is required. The server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` (optional) is origin context only (never fetched).`;\n\nexport function extractSectionHandler(args: unknown): CallToolResult {\n try {\n return extractSection(args);\n } catch (err) {\n logger.error(\n `extract_section failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExtractSectionTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'extract_section',\n {\n title: 'Extract one section by selector or heading',\n description: EXTRACT_SECTION_TOOL_DESCRIPTION,\n inputSchema: extractSectionInputShape,\n outputSchema: outputSchemaShape,\n },\n extractSectionHandler,\n );\n}\n","import type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { applySelectors } from '../pipeline/normalize.js';\nimport {\n parseTableMatrix,\n renderTable,\n resolveHeaderKeys,\n} from '../policy/tables.js';\nimport { readHtmlFile } from './html-source.js';\nimport { extractTablesOutputShape } from './output-schema.js';\nimport {\n type ExtractTablesFromHtmlInput,\n type ExtractTablesInput,\n extractTablesInputSchema,\n extractTablesInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\ninterface ExtractedTable {\n readonly cols: number;\n readonly index: number;\n readonly markdown: string;\n readonly rows: number;\n}\n\nconst NO_TABLES = '(no tables found)';\n\nexport function extractTables(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = extractTablesInputSchema.parse(rawArgs);\n return extractTablesFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\n// Schema defaults for callers that pass only a subset of the knobs (format).\nconst DEFAULTS: Omit<ExtractTablesInput, 'localPath'> =\n extractTablesInputSchema.parse({ localPath: '' });\n\nexport function extractTablesFromHtml(\n input: Readonly<ExtractTablesFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, format, selectors } = { ...DEFAULTS, ...input };\n\n // Skip normalizeDocument/Readability on purpose: this tool exists to reach\n // tables that live outside the scored article (nav, aside, boilerplate), which\n // those stages would discard. Table structure is static HTML, so the matrix\n // walk is unaffected by unsanitized scripts/styles.\n const { document } = buildDocument(html, baseUrl);\n applySelectors(document, selectors);\n\n const tables: ExtractedTable[] = [];\n let index = 0;\n for (const table of document.querySelectorAll('table')) {\n const matrix = parseTableMatrix(table);\n if (matrix.length === 0) {\n continue;\n }\n const keys =\n format === 'json' ? resolveHeaderKeys(table, matrix) : undefined;\n const markdown = renderTable(matrix, format, keys);\n tables.push({\n index,\n rows: matrix.length,\n cols: matrix[0]?.length ?? 0,\n markdown,\n });\n index++;\n }\n\n const content =\n tables.length > 0\n ? tables.map(entry => entry.markdown).join('\\n\\n')\n : NO_TABLES;\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n tables,\n metadata: { baseUrl, format, tableCount: tables.length },\n },\n };\n}\n\nexport const EXTRACT_TABLES_TOOL_DESCRIPTION = `Extract every <table> on the page from already-rendered (post-JavaScript) HTML and return each as GFM / CSV / JSON (caller picks). Runs no Readability, Turndown, or sanitization — a page-wide \\`querySelectorAll('table')\\` walk in front of the same rowspan/colspan-aware matrix serializer used by the \\`tables\\` option on \\`extract\\`. Captures tables outside the article body (nav, aside, boilerplate) that the \\`tables\\` option never sees. The server fetches nothing: \\`localPath\\` is the only source, and \\`baseUrl\\` (optional) is origin context only (never fetched).`;\n\nexport function extractTablesHandler(args: unknown): CallToolResult {\n try {\n return extractTables(args);\n } catch (err) {\n logger.error(\n `extract_tables failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerExtractTablesTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'extract_tables',\n {\n title: 'Extract every table on the page',\n description: EXTRACT_TABLES_TOOL_DESCRIPTION,\n inputSchema: extractTablesInputShape,\n outputSchema: extractTablesOutputShape,\n },\n extractTablesHandler,\n );\n}\n","import type { SanitizationDiagnostics } from '../pipeline/context.js';\nimport type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { formatPayload } from '../output/format.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport {\n applySelectors,\n normalizeDocument,\n resolveLazyImages,\n} from '../pipeline/normalize.js';\nimport { sanitizeHtml } from '../pipeline/sanitize.js';\nimport { toMarkdown } from '../pipeline/turndown.js';\nimport { assembleDiagnostics, TraceCollector } from '../policy/diagnostics.js';\nimport { computeTextMetrics, nonEmpty } from '../policy/text.js';\nimport { truncateMarkdown } from '../policy/truncate.js';\nimport { readHtmlFile } from './html-source.js';\nimport { outputSchemaShape } from './output-schema.js';\nimport {\n type HtmlToMarkdownFromHtmlInput,\n type HtmlToMarkdownInput,\n htmlToMarkdownInputSchema,\n htmlToMarkdownInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\nconst EXTRACTED_NODE = 'fragment';\n\nexport function htmlToMarkdown(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = htmlToMarkdownInputSchema.parse(rawArgs);\n return htmlToMarkdownFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\n// Schema defaults for callers that pass only a subset of the knobs.\nconst DEFAULTS: Omit<HtmlToMarkdownInput, 'localPath'> =\n htmlToMarkdownInputSchema.parse({ localPath: '' });\n\nexport function htmlToMarkdownFromHtml(\n input: Readonly<HtmlToMarkdownFromHtmlInput>,\n): CallToolResult {\n const {\n html,\n baseUrl,\n selectors,\n format,\n metadataMode,\n gfm,\n headingStyle,\n codeBlockStyle,\n images,\n sanitize: shouldSanitize,\n maxChars,\n wordsPerMinute,\n cleanChrome,\n tables,\n debug,\n } = { ...DEFAULTS, ...input };\n\n const trace = new TraceCollector(debug);\n\n const { document, window } = buildDocument(html, baseUrl);\n\n const {\n documentElementCount,\n normalizeCounts,\n imagesResolved,\n textContent,\n rawHtml,\n } = trace.run('normalize', () => {\n const documentElementCount = document.querySelectorAll('*').length;\n const normalizeCounts = normalizeDocument(document, { cleanChrome });\n const imagesResolved = resolveLazyImages(document);\n applySelectors(document, selectors);\n const body = document.body;\n return {\n documentElementCount,\n imagesResolved,\n normalizeCounts,\n rawHtml: body.innerHTML,\n textContent: body.textContent,\n };\n });\n\n const { html: sanitizedHtml, counts: sanitizeCounts } = trace.run(\n 'sanitize',\n (): { counts: SanitizationDiagnostics; html: string } => {\n if (!shouldSanitize) {\n return { counts: { iframes: 0, scripts: 0 }, html: rawHtml };\n }\n const res = sanitizeHtml(rawHtml, window);\n return {\n counts: { iframes: res.iframesRemoved, scripts: res.scriptsRemoved },\n html: res.html,\n };\n },\n );\n const markdown = trace.run('turndown', () =>\n toMarkdown(sanitizedHtml, {\n codeBlockStyle,\n gfm,\n headingStyle,\n images,\n tables,\n baseUrl,\n }),\n );\n\n const { metadata } = trace.run('metadata', () => {\n const firstHeading = nonEmpty(\n document.body.querySelector('h1, h2, h3, h4, h5, h6')?.textContent,\n );\n const metadata = {\n title: firstHeading,\n baseUrl,\n ...computeTextMetrics(textContent, wordsPerMinute),\n };\n return { metadata };\n });\n\n const sanitization: SanitizationDiagnostics = {\n iframes: normalizeCounts.iframes + sanitizeCounts.iframes,\n scripts: normalizeCounts.scripts + sanitizeCounts.scripts,\n };\n const baseDiagnostics = assembleDiagnostics({\n articleHtml: sanitizedHtml,\n boilerplateRemoved: normalizeCounts.boilerplateRemoved,\n chromeRemoved: normalizeCounts.chromeRemoved,\n documentElementCount,\n extractedNode: EXTRACTED_NODE,\n fallbackUsed: true,\n imagesResolved,\n sanitization,\n trace: trace.collect(),\n truncated: false,\n window,\n });\n\n let payload = formatPayload({\n diagnostics: baseDiagnostics,\n format,\n markdown,\n metadata,\n metadataMode,\n sanitizedHtml,\n textContent,\n });\n\n let truncated = false;\n if (maxChars !== undefined && (format === 'markdown' || format === 'text')) {\n const res = truncateMarkdown(payload, maxChars);\n payload = res.text;\n truncated = res.truncated;\n }\n const diagnostics = truncated\n ? { ...baseDiagnostics, truncated }\n : baseDiagnostics;\n\n return {\n content: [{ text: payload, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content: payload,\n metadata,\n diagnostics,\n },\n };\n}\n\nexport const HTML_TO_MARKDOWN_TOOL_DESCRIPTION = `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: \\`localPath\\` is the only source, and \\`baseUrl\\` (optional) absolutizes relative links.`;\n\nexport function htmlToMarkdownHandler(args: unknown): CallToolResult {\n try {\n return htmlToMarkdown(args);\n } catch (err) {\n logger.error(\n `html_to_markdown failed: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerHtmlToMarkdownTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'html_to_markdown',\n {\n title: 'Convert HTML fragment to Markdown',\n description: HTML_TO_MARKDOWN_TOOL_DESCRIPTION,\n inputSchema: htmlToMarkdownInputShape,\n outputSchema: outputSchemaShape,\n },\n htmlToMarkdownHandler,\n );\n}\n","import type { ToolHandle } from '../server.js';\n\nimport { toErrorResult } from '../errors.js';\nimport { logger } from '../logger.js';\nimport { buildDocument } from '../pipeline/dom.js';\nimport { applySelectors, normalizeDocument } from '../pipeline/normalize.js';\nimport { resolveOutline } from '../policy/outline.js';\nimport { readHtmlFile } from './html-source.js';\nimport { outlineOutputShape } from './output-schema.js';\nimport {\n type OutlineFromHtmlInput,\n outlineInputSchema,\n outlineInputShape,\n} from './schemas.js';\n\nimport type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';\n\n// One line per heading, nested by depth; never blank so content[0].text is always scannable.\nfunction renderOutlineToc(\n outline: readonly { level: number; text: string }[],\n): string {\n if (outline.length === 0) {\n return '(no headings found)';\n }\n return outline\n .map(entry => `${' '.repeat(entry.level - 1)}- ${entry.text}`)\n .join('\\n');\n}\n\nexport function outlineDocument(rawArgs: unknown): CallToolResult {\n const { localPath, ...rest } = outlineInputSchema.parse(rawArgs);\n return outlineDocumentFromHtml({ html: readHtmlFile(localPath), ...rest });\n}\n\nexport function outlineDocumentFromHtml(\n input: Readonly<OutlineFromHtmlInput>,\n): CallToolResult {\n const { html, baseUrl, selectors } = input;\n\n const { document } = buildDocument(html, baseUrl);\n normalizeDocument(document);\n applySelectors(document, selectors);\n const outline = resolveOutline(document);\n\n const title =\n document.title.trim() ||\n document.querySelector('h1')?.textContent.replace(/\\s+/g, ' ').trim() ||\n undefined;\n const metadata = { title, baseUrl };\n\n const content = renderOutlineToc(outline);\n return {\n content: [{ text: content, type: 'text' }],\n structuredContent: {\n schemaVersion: 1,\n content,\n outline,\n metadata,\n },\n };\n}\n\nexport const OUTLINE_TOOL_DESCRIPTION = `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: \\`localPath\\` is the only source, and \\`baseUrl\\` is origin context only (never fetched).`;\n\nexport function outlineHandler(args: unknown): CallToolResult {\n try {\n return outlineDocument(args);\n } catch (err) {\n logger.error(\n `outline failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n return toErrorResult(err);\n }\n}\n\nexport function registerOutlineTool(server: McpServer): ToolHandle {\n return server.registerTool(\n 'outline',\n {\n title: 'Get document outline (heading TOC)',\n description: OUTLINE_TOOL_DESCRIPTION,\n inputSchema: outlineInputShape,\n outputSchema: outlineOutputShape,\n },\n outlineHandler,\n );\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\n\nimport { loadConfig } from './config.js';\nimport { registerResources } from './resources.js';\nimport { registerSamplingTools } from './sampling.js';\nimport { registerChunkTextTool } from './tools/chunk_text.js';\nimport { registerExplainTool } from './tools/explain.js';\nimport { registerExtractGridTool } from './tools/extract_grid.js';\nimport { registerExtractLinksTool } from './tools/extract_links.js';\nimport { registerExtractListTool } from './tools/extract_list.js';\nimport { registerExtractMetadataTool } from './tools/extract_metadata.js';\nimport { registerExtractSectionTool } from './tools/extract_section.js';\nimport { registerExtractTablesTool } from './tools/extract_tables.js';\nimport { registerExtractTool } from './tools/extract.js';\nimport { registerHtmlToMarkdownTool } from './tools/html_to_markdown.js';\nimport { registerOutlineTool } from './tools/outline.js';\n\n// Re-exported so the dev hot-reload loop can import server.ts as a single\n// RuntimeModule and pick up every registration family (tools/resources).\nexport { registerResources };\n\n// `remove()` unregisters the tool and notifies the client; dev reload holds the\n// previous batch to remove before re-registering.\nexport interface ToolHandle {\n remove(): void;\n}\n\nexport function createMcpServer(): McpServer {\n const { name, version, title, description, instructions } = loadConfig();\n return new McpServer({ name, version, title, description }, { instructions });\n}\n\n// A tool earns its place only by skipping a pipeline stage (outline,\n// extract_metadata skip Readability) or returning a fundamentally different\n// shape (extract_list is a second engine). Alternate views of the one pipeline\n// — tables, images, structured data, code — are options or fields on `extract`,\n// not separate tools; that keeps the MCP surface small and the pipeline deep.\nexport function registerTools(server: McpServer): ToolHandle[] {\n return [\n registerChunkTextTool(server),\n registerExplainTool(server),\n registerExtractGridTool(server),\n registerExtractLinksTool(server),\n registerExtractListTool(server),\n registerExtractTool(server),\n registerExtractMetadataTool(server),\n registerExtractSectionTool(server),\n registerExtractTablesTool(server),\n registerHtmlToMarkdownTool(server),\n registerOutlineTool(server),\n ];\n}\n\n// Capability-gated tools are registered AFTER the initialize handshake, not\n// eagerly with the families above. The MCP `tools` capability locks in on the\n// first pre-connect registration (registerCapabilities throws post-connect),\n// so the tool-list handlers are already live; adding to `_registeredTools`\n// later simply appears in the next `tools/list` and fires `listChanged`. The\n// low-level Server populates `getClientCapabilities()` from the client's\n// `initialize` request, exposed via `McpServer.server`.\nexport function registerCapabilityGatedTools(server: McpServer): ToolHandle[] {\n const caps = server.server.getClientCapabilities();\n if (!caps?.sampling) {\n return [];\n }\n return registerSamplingTools(server);\n}\n\nexport function createServer(): McpServer {\n const server = createMcpServer();\n registerTools(server);\n registerResources(server);\n // Capability-gated tools (sampling) need the client's advertised\n // capabilities, which the low-level Server only populates after the\n // initialize handshake. Hook the `initialized` notification — it fires once\n // the client has connected and before any `tools/list`. `dev.ts` re-runs the\n // gate directly on reload (the client is already past `initialized`).\n server.server.oninitialized = () => {\n registerCapabilityGatedTools(server);\n };\n return server;\n}\n","import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { createServer } from './server.js';\n\nif (process.argv[2] === 'extract') {\n void import('./cli.js')\n .then(m => m.runCli(process.argv.slice(2)))\n .then(code => {\n // eslint-disable-next-line n/no-process-exit\n process.exit(code);\n })\n .catch((err: unknown) => {\n process.stderr.write(\n `${err instanceof Error ? err.message : String(err)}\\n`,\n );\n // eslint-disable-next-line n/no-process-exit\n process.exit(1);\n });\n} else {\n const server: McpServer = createServer();\n const transport = new StdioServerTransport();\n\n await server.connect(transport);\n\n function shutdown(): void {\n void server\n .close()\n .catch(() => {\n // best-effort; exiting regardless\n })\n .finally(() => {\n // Force-exit on signal; the n/no-process-exit rule targets libraries.\n // eslint-disable-next-line n/no-process-exit\n process.exit(0);\n });\n }\n\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@v1nvn/readability-mcp",
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "MCP server that turns rendered (post-JS) HTML into clean Markdown + metadata via Readability, Turndown, and DOMPurify.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": "dist/index.js",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "vite build",
|
|
10
|
+
"bench": "vite-node test/bench/run.ts",
|
|
11
|
+
"dev": "vite-node src/dev.ts",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
15
|
+
"test:update-goldens": "UPDATE_GOLDENS=1 vitest run",
|
|
16
|
+
"coverage": "vitest run --coverage"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"readability",
|
|
21
|
+
"markdown",
|
|
22
|
+
"turndown",
|
|
23
|
+
"model-context-protocol"
|
|
24
|
+
],
|
|
25
|
+
"author": "v1nvn",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/v1nvn/agentic.git",
|
|
30
|
+
"directory": "packages/readability-mcp"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist"
|
|
34
|
+
],
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
43
|
+
"@mozilla/readability": "^0.6.0",
|
|
44
|
+
"dompurify": "^3.4.12",
|
|
45
|
+
"jsdom": "^29.1.1",
|
|
46
|
+
"remark-gfm": "^4.0.1",
|
|
47
|
+
"remark-parse": "^11.0.0",
|
|
48
|
+
"turndown": "^7.2.4",
|
|
49
|
+
"turndown-plugin-gfm": "^1.0.2",
|
|
50
|
+
"unified": "^11.0.5",
|
|
51
|
+
"yaml": "^2.9.0",
|
|
52
|
+
"zod": "^4.4.3"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/mdast": "^4.0.4",
|
|
56
|
+
"@types/turndown": "^5.0.6",
|
|
57
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
58
|
+
"vite": "^8.1.4",
|
|
59
|
+
"vite-node": "^6.0.0",
|
|
60
|
+
"vitest": "^4.1.10"
|
|
61
|
+
}
|
|
62
|
+
}
|