Diese Seite erklärt, was PullMD macht, wie der Cache funktioniert und wie du den Service in Claude Code oder anderen KI-Agenten einrichtest.
This page explains what PullMD does, how the cache works, and how to wire it into Claude Code or any other AI agent.
In v3 ist PullMD von einem reinen Web-Reader zu einem allgemeinen Alles-zu-Markdown-Dienst gewachsen. Alles über die normale Web-Extraktion hinaus ist optional und lässt sich einzeln aktivieren:
In v3, PullMD grew from a web-page reader into a general anything-to-Markdown service. Everything beyond plain web extraction is optional and enabled individually:
# Titel + Inhalt; Quell-URL, Datum und Metadaten liegen im YAML-Frontmatter, statt im Text wiederholt zu werden (spart Tokens).?pdf=ocr für saubere Tabellen.# Title + content; the source URL, date and metadata live in the YAML frontmatter instead of being repeated in the text (saves tokens).?pdf=ocr for clean tables.X-Transcript-Status unterscheidet bei YouTube ein fehlendes Transkript von einer temporären Sperre.?query= liefert nur die zur Frage passenden Abschnitte statt der ganzen Seite, siehe Query-Extraktion.select.content, um den Artikel-Body direkt zu benennen..md-Datei; den Dateinamen schlägt der Server vor (X-Suggested-Filename), auf Wunsch mit Datums-Präfix.X-Transcript-Status tells a missing YouTube transcript apart from a temporary block.?query= returns only the sections relevant to your question instead of the whole page, see Query extraction.select.content to name the article body outright..md file, named by the server (X-Suggested-Filename), optionally with a date prefix.PullMD ruft eine beliebige URL ab und liefert sie als sauberes Markdown zurück. Je nach Quelle wird der passende Extraktions-Pfad gewählt:
PullMD fetches any URL and returns it as clean Markdown. It picks the right extraction path depending on the source:
Accept: text/markdown nativ unterstützt, wird das direkt durchgereicht (sauberster Output).?render=force bzw. ?render=skip erzwing- oder abschaltbar.Accept: text/markdown natively, that's passed through directly (cleanest output).?render=force / ?render=skip.Zwei Korrekturen laufen darüber: Site-Recipes passen die Extraktion pro Host an (siehe Site-Recipes), und der Coverage Guard greift ein, wenn die Extraktion nur einen Bruchteil einer großen Seite behalten hat, und konvertiert stattdessen den Container mit dem eigentlichen Body. Er kann ein Ergebnis nur vergrößern, meldet sich als source: coverage-guard und lässt sich mit PULLMD_COVERAGE_GUARD=off abschalten.
Two corrections sit on top: site recipes tune extraction per host (see Site recipes), and the coverage guard steps in when an extraction kept only a sliver of a large page, converting the container that actually holds the body instead. It can only grow a result, reports itself as source: coverage-guard, and is switched off with PULLMD_COVERAGE_GUARD=off.
Welcher Pfad genommen wurde, sieht man im Response-Header X-Source und in der History neben jedem Eintrag.
Which path was used shows up in the response header X-Source and next to each entry in the history.
Jeder Pull wird in einer SQLite-Datenbank gespeichert. Zwei Zeitspannen sind wichtig:
Every pull is stored in a SQLite database. Two timeouts matter:
| Was | Wert | Wann zurücksetzen? | What | Value | When does it reset? |
|---|---|---|---|---|---|
| Re-Fetch von der Quelle | 1 Stunde |
Bei jedem erfolgreichen Pull derselben URL — egal ob über /api?url=… oder über /s/:id. |
Re-fetch from source | 1 hour |
On every successful pull of the same URL — regardless of whether it came through /api?url=… or /s/:id. |
| Share-Link-Lebensdauer | 90 Tage |
Bei jedem Re-Fetch (=> Cache schreibt). Auch /s/:id-Aufrufe verlängern, da sie nach 1 h einen Re-Fetch auslösen. |
Share link lifetime | 90 days |
On every re-fetch (= cache write). /s/:id requests extend it too, since they trigger a re-fetch after 1h. |
/s/:id/s/:id behavesPull einmal einen Subreddit-Listing-Link, merke dir die Share-ID — und ruf danach nur noch /s/:id auf. Nach jeder Stunde löst der nächste Aufruf einen frischen Fetch aus, die Share-ID bleibt stabil, der Inhalt aktualisiert sich. Praktisch für KI-Agenten, die einen festen Endpoint mit regelmäßig aktuellem Inhalt brauchen.
Pull a subreddit-listing URL once, remember the share ID — and from then on only call /s/:id. After each hour the next request triggers a fresh fetch, the share ID stays stable, and the content updates. Handy for AI agents that need a fixed endpoint with regularly refreshed content.
Funktioniert in jedem Chat-Agent (ChatGPT, Claude.ai, Gemini, Perplexity, …). Kopiere den Block, füge ihn als System- oder Custom-Instruction ein:
Works in any chat-style agent (ChatGPT, Claude.ai, Gemini, Perplexity, …). Copy the block, paste it as a system or custom instruction:
When you need to read a web page, fetch it via PullMD instead of your
built-in fetch/browse tool - not just when that one fails:
GET https://markdown.invisibletheaters.com}/api?url=<URL>
Returns clean Markdown (text/markdown). Optional query params:
comments=false skip Reddit / Hacker News comments
comment_depth=N comment nesting depth (default 3)
frontmatter=true prepend YAML metadata block
format=text strip Markdown, return plain text
nocache=true bypass the 1h cache and refetch
render=force|skip override the auto headless-Chromium fallback
extractor=readability|trafilatura|playwright force one extractor
pdf=ocr high-quality PDF conversion (tables)
query=<text> got a specific question? pass it and get back only
the matching sections (70-95% fewer tokens)
max_tokens=N budget for query= (default 600, 64-20000)
lang=de|en language for the comments section header
Response headers worth checking:
X-Source reddit | hackernews | cloudflare | readability |
trafilatura | playwright | recipe-content |
coverage-guard | markitdown | youtube | pdf-ocr | ...
X-Quality 0.0-1.0 extraction confidence
X-Share-Id 8-hex permalink, openable as /s/<id>
X-Transcript-Status youtube only: ok | none | blocked | error
(blocked/error = transient, not cached — retry later)
Reddit URLs are auto-detected (incl. redd.it short links and /s/ shares).
Hacker News URLs are auto-detected too — items, comment permalinks, and the
front/newest/ask/show/jobs listings.
Use this whenever you would otherwise fetch raw HTML — the markdown is
much cleaner and saves significant context window space. When you only
need specific information rather than the whole document, pass your
question as query= and get back just the relevant sections.
Für Claude Code gibt es eine fertige Skill, die WebFetch automatisch durch PullMD ersetzt (mit Fallback). Lade sie als Zip und entpacke nach ~/.claude/skills/:
For Claude Code there's a ready-made skill that automatically routes WebFetch through PullMD (with fallback). Download the zip and unpack into ~/.claude/skills/:
pullmd.zip herunterladenDownload pullmd.zip
curl -O https://markdown.invisibletheaters.com}/pullmd.zip mkdir -p ~/.claude/skills unzip pullmd.zip -d ~/.claude/skills/ # Restart Claude Code; the skill activates on web-reading requests.
Upgrade von vor v3: Die Skill hieß früher web-reader. Das neue Zip ersetzt eine bestehende Installation nicht — erst die alte entfernen (rm -rf ~/.claude/skills/web-reader), sonst sind beide Skills parallel aktiv.
Upgrading from pre-v3: the skill used to be called web-reader. The new zip does not replace an existing install — remove the old one first (rm -rf ~/.claude/skills/web-reader), otherwise both skills stay active side by side.
PullMD läuft als remote MCP-Server unter https://markdown.invisibletheaters.com}/mcp (Streamable-HTTP-Transport, stateless). Drei Tools: read_url, get_share, list_recent. Server-seitige Updates erreichen automatisch alle Clients — keine lokale Installation nötig.
PullMD runs as a remote MCP server at https://markdown.invisibletheaters.com}/mcp (Streamable-HTTP transport, stateless). Three tools: read_url, get_share, list_recent. Server-side updates reach every client automatically — no local install needed.
Claude Code — Prompt einfügen, Claude installiert es selbst:
Claude Code — paste this prompt and Claude will install it for you:
Installiere den PullMD MCP-Server in Claude Code (User-Scope):
- Name: pullmd
- Transport: http
- URL: https://markdown.invisibletheaters.com}/mcp
Nutze: claude mcp add --transport http pullmd https://markdown.invisibletheaters.com}/mcp
Danach: claude mcp list zur Verifikation.
Install the PullMD MCP server in Claude Code (user scope):
- Name: pullmd
- Transport: http
- URL: https://markdown.invisibletheaters.com}/mcp
Run: claude mcp add --transport http pullmd https://markdown.invisibletheaters.com}/mcp
Then: claude mcp list to verify.
Claude Code — direkt im Terminal:
Claude Code — directly in the terminal:
claude mcp add --transport http pullmd https://markdown.invisibletheaters.com}/mcp
Claude Desktop / Cursor / andere — JSON-Konfig:
Claude Desktop / Cursor / others — JSON config:
{
"mcpServers": {
"pullmd": {
"type": "http",
"url": "https://markdown.invisibletheaters.com}/mcp"
}
}
}
Sobald registriert, erscheinen die drei Tools nativ im Agent — keine Prompt-Anweisungen nötig, das LLM erkennt sie über ihre Schema-Beschreibungen.
Once registered, the three tools surface natively in the agent — no prompt instructions needed, the LLM picks them up via their schema descriptions.
read_url kennt dieselben Stellschrauben wie die REST-API: comments, comment_depth, comment_limit, frontmatter, lang, nocache, extractor, yt_timecodes, yt_chunk, pdf_ocr (statt pdf=ocr), query und max_tokens. MCP-Antworten haben keine Response-Header; findet die Query-Extraktion nichts, steht stattdessen ein Kommentar im Markdown.
read_url exposes the same knobs as the REST API: comments, comment_depth, comment_limit, frontmatter, lang, nocache, extractor, yt_timecodes, yt_chunk, pdf_ocr (instead of pdf=ocr), query and max_tokens. MCP responses carry no headers; when query extraction finds no match it prepends an in-band comment to the markdown instead.
Instanz mit Login? Dann brauchen /api und /mcp Zugangsdaten: einen API-Key als Authorization: Bearer pmd_… oder OAuth für Claude Desktop und claude.ai. Details unter Authentifizierung.
Instance with login? Then /api and /mcp need credentials: an API key as Authorization: Bearer pmd_…, or OAuth for Claude Desktop and claude.ai. Details under Authentication.
Alles, was die Instanz nach außen anbietet. GET /api ist der Hauptweg, der Rest ist Beiwerk für PWA, Betrieb und Integration.
Everything the instance exposes. GET /api is the main path; the rest supports the PWA, operations, and integration.
| Endpoint | Zweck | Purpose |
|---|---|---|
GET /api?url=… |
URL zu Markdown. Siehe API-Parameter. | URL to Markdown. See API parameters. |
GET /api/stream?url=… |
Dieselbe Konvertierung als Server-Sent-Events: status-Events pro Extraktionsstufe, dann ein result- oder error-Event. Die PWA zeigt damit den Fortschritt an. |
The same conversion as Server-Sent Events: a status event per extraction stage, then one result or error event. This is what drives the PWA's progress display. |
POST /api/html |
Gespeicherte HTML-Datei konvertieren (max. 10 MB). | Convert a saved HTML file (max 10 MB). |
POST /api/file |
Dokument-Upload konvertieren (max. 25 MB). | Convert an uploaded document (max 25 MB). |
GET /s/:id |
Share-Link, aktualisiert sich selbst. Siehe Share-Links. | Share link, refreshes itself. See Share links. |
GET /api/history |
Letzte Konversionen als JSON (?limit=, Default 20, max. 100). |
Recent conversions as JSON (?limit=, default 20, max 100). |
GET /api/archive |
Seitenweise durch das ganze Archiv (?limit= Default 50, max. 200, ?offset=). |
Paginated full archive (?limit= default 50, max 200, ?offset=). |
GET /api/storage |
Cache-Größe und Aufbewahrungsdauer. | Cache size and retention. |
GET /api/stats |
Extraktions-Telemetrie: Quellen, Qualität, Laufzeit (?window=-7 days). |
Extraction telemetry: sources, quality, latency (?window=-7 days). |
GET /api/config |
Was auf dieser Instanz aktiv ist (Auth-Modus, Dokumente, Vision, STT, PDF-OCR, YouTube). Die PWA blendet danach ihre Bedienelemente ein oder aus. | What is enabled on this instance (auth mode, documents, vision, STT, PDF OCR, YouTube). The PWA shows or hides its controls accordingly. |
GET /api/recipes/status |
Welche Site-Recipes geladen bzw. abgelehnt wurden. | Which site recipes loaded or were rejected. |
POST /mcp |
MCP-Server (Streamable-HTTP, stateless), drei Tools. | MCP server (Streamable HTTP, stateless), three tools. |
GET /pullmd.zip |
Claude-Code-Skill mit der URL dieser Instanz. /web-reader.zip leitet hierher weiter. |
Claude Code skill with this instance's URL baked in. /web-reader.zip redirects here. |
Zum Schutz vor Server-Side Request Forgery (SSRF) werden Anfragen an private, loopback-, link-local-, CGNAT- und Cloud-Metadata-Adressen (z. B. 169.254.169.254, 100.100.100.200) standardmäßig abgelehnt - auch wenn eine URL erst per Redirect dorthin führt und auch über IPv6-Übergangsadressen. /api und das MCP-Tool read_url prüfen vorab und antworten mit 403, bevor irgendetwas geholt wird; /api/stream meldet die Ablehnung als error-Event. Selbst-Hoster können mit PULLMD_ALLOWED_HOSTS (kommagetrennte CIDRs und/oder Hostnamen) gezielt interne Ziele freigeben.
To guard against Server-Side Request Forgery (SSRF), requests to private, loopback, link-local, CGNAT and cloud-metadata addresses (e.g. 169.254.169.254, 100.100.100.200) are rejected by default - including when a URL only reaches them via redirect, and including IPv6 transition addressing. /api and the MCP read_url tool check up front and answer 403 before fetching anything; /api/stream surfaces the refusal as an error event. Self-hosters can allow specific internal targets via PULLMD_ALLOWED_HOSTS (comma-separated CIDRs and/or hostnames).
| Param | Default | Beschreibung | Default | Description |
|---|---|---|---|---|
url | — | Pflicht. Beliebige öffentliche URL. | Required. Any public URL. | |
comments | true |
Kommentare einschließen (Reddit und Hacker News). false liefert nur den Post. Bei anderen URLs wirkungslos. |
Include comments (Reddit and Hacker News). false returns just the post. No effect on other URLs. |
|
comment_depth | 3 |
Maximale Verschachtelungstiefe (1–10), für Reddit und Hacker News. | Max nesting depth (1–10), for Reddit and Hacker News. | |
comment_limit | — | Optionale Obergrenze für Top-Level-Kommentare (Reddit liefert standardmäßig ~200). | Optional cap on top-level comments (Reddit returns ~200 by default). | |
frontmatter | false |
YAML-Frontmatter mit Metadaten voranstellen. | Prepend YAML frontmatter with metadata. | |
format | md |
text = Markdown-Formatierung entfernen, Plaintext zurückgeben. json = strukturiert mit Metadaten. |
text = strip Markdown, return plain text. json = structured with metadata. |
|
nocache | false |
1-h-Cache umgehen, immer frisch holen. | Bypass the 1-hour cache, always refetch. | |
render | auto | force = immer im Headless-Browser rendern, skip = nie. Umgeht den Cache. |
force = always render in the headless browser, skip = never. Bypasses the cache. |
|
extractor | auto | Erzwingt readability, trafilatura oder playwright und überspringt die Qualitätsauswahl. Notausgang, wenn die Automatik bei einer Seite danebengreift. Umgeht den Cache; bei Reddit wirkungslos. |
Forces readability, trafilatura or playwright and skips the quality pick. An escape hatch for sites where the automatic choice is wrong. Bypasses the cache; ignored for Reddit. |
|
pdf | — | ocr schickt PDFs durch die hochwertige OCR-Stufe (saubere Tabellen). Braucht einen serverseitigen OCR-Key; ohne ihn bleibt es beim normalen Pfad. Umgeht den Cache. |
ocr routes PDFs through the high-quality OCR tier (clean tables). Needs a server-side OCR key; without one the normal path is used. Bypasses the cache. |
|
yt_timecodes | links |
YouTube-Transkript: links = klickbare Zeitstempel, plain = [MM:SS], none = nur Text. Umgeht den Cache. |
YouTube transcript: links = clickable timestamps, plain = [MM:SS], none = text only. Bypasses the cache. |
|
yt_chunk | 30 |
Blockgröße des Transkripts in Sekunden; 0 behält die Original-Schnipsel. Umgeht den Cache. |
Transcript block size in seconds; 0 keeps the original snippets. Bypasses the cache. |
|
lang | de |
Sprache des Kommentar-Headers (de oder en). |
Language for the comments header (de or en). |
|
query | — | Für eine konkrete Frage an eine lange Seite: Frage hier übergeben, zurück kommen nur die passenden Abschnitte statt der ganzen Seite, siehe Query-Extraktion. | For a specific question about a long page: pass the question here and get back only the matching sections instead of the whole page, see Query extraction. | |
max_tokens | 600 |
Token-Budget für query (64–20000). Ohne query wirkungslos; höher setzen, wenn die Antwort über mehrere Abschnitte verteilt liegt. Wird nur geprüft, wenn query gesetzt ist; ein ungültiger Wert liefert 400. |
Token budget for query (64–20000). No effect without query; raise it when the answer likely spans several sections. Only validated when query is set; an invalid value returns 400. |
Dieselben Parameter gelten für GET /api/stream. Alles, was das erwartete Ergebnis verändert (comment_depth, comment_limit, render, extractor, pdf=ocr, yt_*), umgeht den Cache, damit der neue Wert auch wirklich greift.
The same parameters apply to GET /api/stream. Anything that changes the expected result (comment_depth, comment_limit, render, extractor, pdf=ocr, yt_*) bypasses the cache so the new value actually takes effect.
Jede Antwort von /api trägt mit, wie sie zustande kam. Für Agenten ist das der billigste Weg, die Qualität einer Extraktion zu beurteilen, ohne den Text zu analysieren.
Every /api response carries how it was produced. For agents that is the cheapest way to judge an extraction without analyzing the text.
| Header | Inhalt | Contents |
|---|---|---|
X-Source |
reddit · hackernews · cloudflare · readability · readability-fallback · trafilatura · playwright · recipe-content · coverage-guard · markitdown · youtube · image-caption · audio-transcript · pdf-ocr |
reddit · hackernews · cloudflare · readability · readability-fallback · trafilatura · playwright · recipe-content · coverage-guard · markitdown · youtube · image-caption · audio-transcript · pdf-ocr |
X-Quality |
0.0–1.0, Konfidenz der Extraktion. Niedrige Werte heißen dünner oder verrauschter Text. |
0.0–1.0 extraction confidence. Low values mean thin or noisy output. |
X-Share-Id |
8-stellige Hex-ID, aufrufbar als /s/<id>. Fehlt bei /api/html und /api/file, weil lokale Dateien nicht gecacht werden. |
8-hex id, openable as /s/<id>. Absent on /api/html and /api/file, since local files are never cached. |
X-Suggested-Filename |
Vorgeschlagener Dateiname für den Download, z. B. YT-vortrag-titel-dQw4w9WgXcQ.md. YouTube bekommt Titel plus Video-ID, Bilder, Audio und Dokumente den ursprünglichen Dateinamen, alles andere den Titel. Self-Hoster können mit PULLMD_FILENAME_DATE_PREFIX ein Datum voranstellen (Vorlage mit den Platzhaltern YYYY MM DD HH mm ss, z. B. YYYY-MM-DD-). Der Download-Button in der Web-Oberfläche speichert das Ergebnis unter genau diesem Namen. Bei /api/stream gibt es keine Header, dort steckt derselbe Wert als suggestedFilename im result-Event. |
Suggested download filename, e.g. YT-talk-title-dQw4w9WgXcQ.md. YouTube gets title plus video id, images, audio and documents keep their original file name, everything else uses the title. Self-hosters can prepend a date with PULLMD_FILENAME_DATE_PREFIX (a template with the tokens YYYY MM DD HH mm ss, e.g. YYYY-MM-DD-). The download button in the web UI saves the result under exactly this name. /api/stream has no headers, so there the same value travels as suggestedFilename in the result event. |
X-Transcript-Status |
Nur YouTube: ok · none · blocked · error. blocked (Rate-Limit, HTTP 429) und error sind vorübergehend und werden nicht gecacht - später erneut versuchen. none heißt, das Video hat wirklich kein Transkript. |
YouTube only: ok · none · blocked · error. blocked (rate limit, HTTP 429) and error are transient and not cached - retry later. none means the video genuinely has no transcript. |
X-Extracted |
true/false. Nur vorhanden, wenn query aktiv ist. |
true/false. Present only when query is active. |
X-Extract-Confidence |
high · medium · low. Fehlt, wenn die Seite ohnehin klein genug war und die Extraktion übersprungen wurde. |
high · medium · low. Absent when the page was small enough that extraction was skipped. |
X-Extract-Sections |
Anzahl der zurückgelieferten Abschnitte. | Number of returned sections. |
X-Extract-Original-TokensX-Extract-Returned-Tokens |
Geschätzte Tokenzahl (Zeichen / 4) der ganzen Seite und der Antwort. Die Differenz ist die Ersparnis. | Estimated token count (chars / 4) of the full page and of the response. The difference is what you saved. |
MCP-Antworten haben keine Header. Dort landen dieselben Angaben im Frontmatter bzw. bei format=json im extract-Objekt.
MCP responses have no headers. There the same data lands in the frontmatter, or in the extract object with format=json.
Wer eine konkrete Frage an eine lange Seite hat, braucht selten die ganze Seite. ?query= nimmt die Frage in natürlicher Sprache entgegen und liefert nur die passenden Abschnitte - auf langen Seiten typischerweise 70-95 % weniger Tokens. Die ganze Seite lohnt sich dann noch, wenn man sie wirklich komplett braucht: zusammenfassen, übersetzen, archivieren.
When you have a specific question about a long page, you rarely need the whole page. ?query= takes that question in natural language and returns only the matching sections - typically 70-95% fewer tokens on long pages. Fetching the full page still makes sense when you genuinely need all of it: summarizing, translating, archiving.
curl -s "https://markdown.invisibletheaters.com}/api?url=https://example.com/long-article&query=how+does+caching+work&max_tokens=800"
max_tokens begrenzt die Ausgabe (Default 600, 64–20000, geschätzt als Zeichen / 4).<!-- … -->.confidence: low).query (oder mit leerem Wert) ist die Antwort byte-identisch zu vorher - keine zusätzlichen Header, keine zusätzlichen Felder.max_tokens caps the output (default 600, 64–20000, estimated as chars / 4).<!-- … -->.confidence: low in the first case).query (or with an empty value) the response is byte-identical to before - no extra headers, no extra fields.Mit ?frontmatter=true kommen extracted, extract_confidence, sections_selected, original_tokens und returned_tokens dazu; format=json liefert dieselben Werte im extract-Objekt.
With ?frontmatter=true you also get extracted, extract_confidence, sections_selected, original_tokens and returned_tokens; format=json returns the same values in the extract object.
Nicht alles steht als abrufbare URL im Netz. Zwei Upload-Wege decken den Rest ab. Beide landen aus Datenschutzgründen nicht im Cache - kein History-Eintrag, kein Share-Link, keine X-Share-Id.
Not everything lives at a fetchable URL. Two upload paths cover the rest. For privacy, neither is ever cached - no history entry, no share link, no X-Share-Id.
Bereits gespeicherte Seiten ("Seite speichern unter", SingleFile-Exports) lassen sich direkt konvertieren: die .html-Datei auf die PullMD-Oberfläche ziehen (Desktop) oder den gestrichelten Hinweis unter dem URL-Feld antippen, um eine Datei zu wählen (Desktop + Mobile) - oder per API:
Already-saved pages ("Save Page As", SingleFile exports) can be converted directly: drag-and-drop the .html file onto the PullMD UI (desktop) or tap the dashed hint below the URL field to pick a file (desktop and mobile) - or via the API:
curl -s -X POST --data-binary @page.html \ -H 'Content-Type: text/html' \ -H 'X-Filename: page.html' \ "https://markdown.invisibletheaters.com}/api/html"
Optionale Parameter: url=… (Original-URL - aktiviert Site-Recipes und den verlinkten Header), format=json|text, frontmatter=true, extractor=readability|trafilatura. Statt des Headers X-Filename (URI-encodiert) geht auch ?filename=; der Header hält den Dateinamen aus Access-Logs heraus. Maximal 10 MB. Findet die Extraktion so gut wie keinen Inhalt - typisch für eine gespeicherte JavaScript-App-Hülle - kommt 422 mit der Bitte, stattdessen die Original-URL zu schicken.
Optional parameters: url=… (original URL - enables site recipes and the linked header), format=json|text, frontmatter=true, extractor=readability|trafilatura. Instead of the X-Filename header (URI-encoded) you can use ?filename=; the header keeps the file name out of access logs. Max 10 MB. If extraction finds almost no content - typical for a saved JavaScript app shell - you get a 422 asking for the original URL instead.
PDF, Word, PowerPoint, Excel, EPUB, ZIP (Inhalt wird aufgelistet), CSV, JSON und XML werden zu Markdown konvertiert. Liegt das Dokument im Netz, reicht die normale API - der Dokumenttyp wird am Content-Type erkannt:
PDF, Word, PowerPoint, Excel, EPUB, ZIP (contents listed), CSV, JSON and XML are converted to Markdown. If the document is online, the regular API is enough - the type is detected from the content type:
curl -s "https://markdown.invisibletheaters.com}/api?url=https://example.com/report.pdf"
Lokale Dateien gehen per Upload - rohe Bytes im Body, Content-Type der Datei, maximal 25 MB. In der Web-Oberfläche funktioniert dafür derselbe Drag-and-drop bzw. die Dateiauswahl:
Local files go through the upload path - raw bytes in the body, the file's content type, max 25 MB. In the web UI the same drag-and-drop and file picker handle it:
curl -s -X POST --data-binary @report.pdf \ -H 'Content-Type: application/pdf' \ -H 'X-Filename: report.pdf' \ "https://markdown.invisibletheaters.com}/api/file"
Optionale Parameter: format=json|text, frontmatter=true, pdf=ocr. Die Dokument-Konvertierung braucht den markitdown-Sidecar; ist er nicht konfiguriert, antworten Dokument-URLs und /api/file mit 502. Ob diese Instanz ihn hat, verrät /api/config.
Optional parameters: format=json|text, frontmatter=true, pdf=ocr. Document conversion needs the markitdown sidecar; without it, document URLs and /api/file return 502. Whether this instance has it is reported by /api/config.
Bilder und Audio funktionieren über denselben Weg (URL oder Upload). Standardmäßig kommen nur Metadaten zurück - EXIF beim Bild, Track-Infos beim Audio. Hat die Instanz ein Vision- bzw. Speech-to-Text-Modell hinterlegt, kommt eine Bildbeschreibung bzw. ein Transkript dazu; die verwendeten Modelle und der Tokenverbrauch stehen dann im Frontmatter. YouTube-URLs liefern Titel, Beschreibung und Transkript, wenn die Instanz das aktiviert hat.
Images and audio work the same way (URL or upload). By default you only get metadata - EXIF for images, track info for audio. If the instance has a vision or speech-to-text model configured, you also get a caption or a transcript; the models used and the token spend then show up in the frontmatter. YouTube URLs return title, description and transcript when the instance enables it.
Mit ?frontmatter=true wird vor dem Inhalt ein YAML-Block mit Metadaten eingefügt. Felder mit leerem Wert werden weggelassen:
With ?frontmatter=true a YAML metadata block is prepended to the content. Empty fields are omitted:
--- title: "Why I migrated my side-project from Postgres to SQLite" url: https://news.ycombinator.com/item?id=42424242 source: readability fetched: 2026-04-25T13:53:00Z quality: 0.85 author: kentonv published: 2026-04-24T18:42:00Z description: "After two years on managed Postgres..." language: en share_id: a3f9c2b7 ---
Basis-Felder: title, url, source, fetched, quality, author, published, modified, description, language, image, site, extractor_reason, share_id. Je nach Quelle zusätzlich: subreddit, upvotes (Reddit) · duration, views (YouTube) · image_size, audio_seconds, llm_model, llm_tokens, llm_prompt_tokens, llm_completion_tokens (Media) · pdf_pages (PDF-OCR). MCP-Antworten ergänzen share_url, cached, refreshed, age_ms. Mit PULLMD_FRONTMATTER_FIELDS lässt sich die Auswahl serverseitig einschränken.
Base fields: title, url, source, fetched, quality, author, published, modified, description, language, image, site, extractor_reason, share_id. Depending on the source, additionally: subreddit, upvotes (Reddit) · duration, views (YouTube) · image_size, audio_seconds, llm_model, llm_tokens, llm_prompt_tokens, llm_completion_tokens (media) · pdf_pages (PDF OCR). MCP responses add share_url, cached, refreshed, age_ms. PULLMD_FRONTMATTER_FIELDS can trim the selection server-side.
Bei aktiver Query-Extraktion kommen extracted, extract_confidence, sections_selected, original_tokens und returned_tokens dazu. Site-Recipes können weitere Felder beisteuern, etwa aus dem JSON-LD einer Seite.
With query extraction active, extracted, extract_confidence, sections_selected, original_tokens and returned_tokens are added. Site recipes can contribute further fields, e.g. from a page's JSON-LD.
In der Web-App schaltet der Frontmatter-Regler die Anzeige sofort um - der YAML-Block erscheint oder verschwindet, ohne dass man erneut auf Pull drücken muss.
In the web app the Frontmatter toggle switches the view instantly - the YAML block appears or disappears with no second Pull.
Manche Seiten brauchen Nachhilfe: eine Paywall-Hülle, ein Cookie-Banner mitten im Text, ein Layout, in dem die Automatik den falschen Block für den Artikel hält. Site-Recipes sind kleine JSON-Einträge pro Host, die genau das geradebiegen.
Some sites need a nudge: a paywall wrapper, a cookie banner in the middle of the text, a layout where the automatic pick grabs the wrong block. Site recipes are small per-host JSON entries that straighten exactly that out.
select.remove) - seitenspezifische Banner, Teaser, Skelette.select.content) - statt die Automatik raten zu lassen, sagt das Recipe direkt, welche Container den Artikel bilden. Ergebnis trägt source: recipe-content.select.remove) - site-specific banners, teasers, skeletons.select.content) - instead of letting the automatic pick guess, the recipe says which containers make up the article. Output carries source: recipe-content.Ein paar Recipes werden mitgeliefert. Selbst-Hoster legen eigene unter data/site-recipes.json ab oder setzen PULLMD_SITE_RECIPES auf einen Pfad; nach Änderungen muss die Instanz neu starten. Was geladen wurde und was abgelehnt, zeigt GET /api/recipes/status. Wie man ein Recipe schreibt (und beisteuert), steht in SITE-RECIPES.md.
A few recipes ship with PullMD. Self-hosters add their own in data/site-recipes.json or point PULLMD_SITE_RECIPES at a path; the instance needs a restart after changes. GET /api/recipes/status reports what loaded and what was rejected. How to write (and contribute) one is documented in SITE-RECIPES.md.
Standardmäßig ist eine PullMD-Instanz offen. Self-Hoster können das umstellen; welcher Modus hier läuft, steht in /api/config unter authMode.
By default a PullMD instance is open. Self-hosters can change that; the mode in use here is reported by /api/config as authMode.
| Modus | Bedeutung | Meaning |
|---|---|---|
disabled |
Default. Kein Login, alles offen. | Default. No login, everything open. |
single-admin |
Ein Benutzer aus den Server-Einstellungen, keine Selbstregistrierung. Für den Heimserver. | One user from the server settings, no self-signup. For a homelab. |
multi-user |
Registrierung unter /signup, Login unter /login, getrennte Daten pro Benutzer. Die Registrierung lässt sich mit PULLMD_ALLOW_SIGNUP=false schließen. |
Sign up at /signup, log in at /login, per-user data isolation. Self-signup can be closed with PULLMD_ALLOW_SIGNUP=false. |
Ist ein Modus aktiv, brauchen /api, /api/stream, die Upload-Endpunkte, /mcp sowie History und Archiv Zugangsdaten. Öffentlich bleiben die Startseite, diese Hilfeseite, das Skill-Zip und vor allem die Share-Links unter /s/:id - geteilte Inhalte bleiben also teilbar.
With a mode active, /api, /api/stream, the upload endpoints, /mcp, history and archive all need credentials. The start page, this help page, the skill zip and above all the share links at /s/:id stay public - shared content stays shareable.
/settings erzeugen und als Authorization: Bearer pmd_… mitschicken. Der Schlüssel wird nur einmal angezeigt, danach liegt serverseitig nur noch sein Hash. Das ist der Weg für curl, Skripte und die Claude-Code-CLI.https://markdown.invisibletheaters.com}/mcp und landet beim ersten Klick auf der Login- und Zustimmungsseite dieser Instanz./settings and send it as Authorization: Bearer pmd_…. The key is shown once; only its hash is kept server-side. This is the path for curl, scripts and the Claude Code CLI.https://markdown.invisibletheaters.com}/mcp and the first click takes you to this instance's login and consent screen.curl -s -H "Authorization: Bearer pmd_YOURKEY" \ "https://markdown.invisibletheaters.com}/api?url=https://example.com" claude mcp add --transport http pullmd https://markdown.invisibletheaters.com}/mcp \ --header "Authorization: Bearer pmd_YOURKEY"
Fehlen die Zugangsdaten, antwortet die API mit 401; im Browser wird stattdessen zur Login-Seite weitergeleitet und danach zurück zur ursprünglichen Seite.
Without credentials the API answers 401; in the browser you are redirected to the login page and back afterwards.
Konten verwaltet der Betreiber auf der Kommandozeile der Instanz; eine Oberfläche dafür gibt es nicht. In single-admin stammt der eine Benutzer aus den Server-Einstellungen, in multi-user registrieren sich Benutzer selbst - es sei denn, PULLMD_ALLOW_SIGNUP steht auf false. Dann ist create-user der Weg, trotzdem Konten anzulegen.
Accounts are managed by the operator on the instance's command line; there is no UI for it. In single-admin the single user comes from the server settings, in multi-user users sign up themselves - unless PULLMD_ALLOW_SIGNUP is false. In that case create-user is how accounts still get created.
docker compose exec pullmd node scripts/admin.js list-users docker compose exec pullmd node scripts/admin.js create-user someone@example.com docker compose exec pullmd node scripts/admin.js make-admin someone@example.com docker compose exec pullmd node scripts/admin.js reset-password someone@example.com
list-users - alle Konten mit ID, E-Mail, Admin-Flag und Anlagedatum.create-user <email> - legt ein Konto an und fragt das Passwort interaktiv ab (mindestens 8 Zeichen). Das Konto bekommt keine Admin-Rechte.make-admin <email> - befördert ein bestehendes Konto zum Admin. Bewusst ein eigener Schritt.reset-password <email> - setzt das Passwort neu und wirft dabei alle offenen Sitzungen des Benutzers weg.list-users - every account with id, email, admin flag and creation date.create-user <email> - creates an account and prompts for the password interactively (8 characters minimum). The account gets no admin rights.make-admin <email> - promotes an existing account to admin. Deliberately a separate step.reset-password <email> - sets a new password and drops all of that user's open sessions.Ohne Docker läuft derselbe Befehl als node scripts/admin.js … im Verzeichnis der Instanz. Wichtig: create-user und reset-password lesen das Passwort von der Standardeingabe, es braucht also docker compose exec oder docker exec -it. Ein docker exec ohne -i hängt die Eingabe nicht an; der Befehl bricht dann mit einer Meldung und Code 2 ab und nennt die funktionierenden Aufrufe. Alternativ lässt sich das Passwort hineinpipen: echo "…" | docker exec -i <container> node scripts/admin.js ….
Without Docker the same command runs as node scripts/admin.js … in the instance's directory. Important: create-user and reset-password read the password from standard input, so use docker compose exec or docker exec -it. A plain docker exec without -i does not attach input; the command then aborts with a message and exit code 2 that names the invocations which do work. You can also pipe the password in: echo "…" | docker exec -i <container> node scripts/admin.js ….
Jeder Eintrag in der History kriegt ein Client-Badge — daran sieht man, woher der Pull kam:
Every history entry gets a client badge so you can see where the pull originated: