Ask an LLM to read a PDF and it cannot; ask it to read markdown and it can. That one-sentence gap is why microsoft/markitdown became infrastructure: a small MIT-licensed library that turns documents into markdown, used everywhere from RAG ingestion scripts to coding agents that need to see what is inside a file. What earns it a source tour is the engineering hiding under the plain façade. The core is a dispatch engine barely seven hundred lines long, surrounded by twenty-five converters that each understand one format deeply — Word documents with OMML equations translated to LaTeX, PowerPoint shapes walked slide by slide, Excel sheets rendered as real tables, Outlook message files parsed with attachments, YouTube pages stripped to transcripts. The architecture is a textbook of extension design: one accepts-convert contract, plugins loaded through Python entry points, and an official OCR package that does not fork the converters but subclasses them. And the monorepo ends with a proof of the design’s tightness — the MCP server that gives coding agents access to all of it is one hundred nineteen lines.
The architecture is a monorepo of four packages. The core package holds the MarkItDown class — a converter registry, a source resolver that accepts paths, URLs, data URIs, and streams, and the CLI. The converters live one directory deep, each a self-contained module implementing two methods: accepts, which judges whether it can handle a file based on its stream info, and convert, which does the work. A second ring of converters reaches out to the live web — YouTube, Bing result pages, RSS feeds, Wikipedia — turning network fetches into the same markdown output as local files. The extension packages demonstrate the two extension axes: a sample plugin shows the entry-point mechanism any third party can use, while the OCR package registers subclasses of the document converters so scanned pages get recognized text without touching core code. The MCP package wraps the engine in one tool, with test suites pinning its transport behavior. As always in this series, what follows is an educational tour of published source code.
MarkItDown at a glance: the CLI drives the engine, the engine dispatches through the accepts-convert contract to document and web converters, the OCR package upgrades conversion as a plugin, and the MCP server exposes the whole fleet to agents.
Reading the overview from left to right:
- The engine routes sources to converters and normalizes the markdown out.
- The command line wraps the engine with format hints and output options.
- The converter contract is accepts and convert, nothing more.
- The PDF converter extracts page by page and defers to plugins for scans.
- The Word converter maps styles to headings and equations to LaTeX.
- The image converter reads EXIF and can caption through a vision model.
- The OCR package subclasses the office converters with a recognition service.
- The MCP server exposes one tool over stdio, SSE, or streamable HTTP.
Why You Need This
The first reason is the contract, because it is the cleanest small example of plugin architecture in the LLM tooling world. The base converter defines accepts and convert; the engine keeps a list, tries each in order, and raises a clear failure if none accept. The subtlety lives in the inputs: StreamInfo carries mimetype, charset, filename, and extension so accepts can judge without reading the whole file, and the URI helpers classify whether the source is a URL, a local path, or a data URI before anything is fetched. Registration happens three ways — built-in converters registered in the constructor, third-party plugins loaded through Python entry points as the sample plugin demonstrates, and direct register_converter calls for embedding. The result is a library where adding a format never means editing the engine, which is exactly why twenty-five formats coexist without the codebase becoming a swamp.
The second reason is the converters themselves, because they encode years of “what actually matters when a model reads this format.” The Word converter maps Word styles to markdown structure and delegates the genuinely hard parts to helpers: the OMML translator walks Word’s equation XML into LaTeX with a symbol dictionary, and the image extractor emits markdown references for embedded media. The PowerPoint converter walks shape trees and speaker notes so slide decks become readable narratives. The PDF converter keeps its dependency footprint light and offers a plugin upgrade path for scanned pages. On the media side, the image converter pulls EXIF metadata and, when given an LLM client through the caption helper, generates alt-text for pictures no document mentioned — a small feature with outsized value for accessibility and retrieval alike.
The third reason is the extension story, because the OCR package shows how to extend a library without forking it. Rather than modifying the converters, the OCR plugin registers subclasses: the OCR PDF converter extends the original, rasterizes pages, and runs them through a recognition service wrapping a local model, and siblings do the same for Word, PowerPoint, and Excel — each proven by inheritance test suites. The same pattern reaches the cloud through the Document Intelligence converter, which swaps local extraction for Azure’s layout-aware service when you configure an endpoint. The endpoint of this design is the MCP server: because the engine already does everything, the server only maps one tool onto it and selects a transport — one hundred nineteen lines, with transport and error test suites keeping the protocol behavior pinned.
The detail view: the engine with stream info and URI resolution; the document formats with their math and media helpers; media and web converters from images and audio to YouTube and RSS; the extension surface of plugins, OCR and cloud extraction; and the MCP package with its tests.
The detail view shows the dispatch structure plainly. Every converter edge returns to the same base contract — office formats with their helpers on one side, web sources on the other, media in between — so the engine’s dispatch loop is the only place that knows them all. The extension group is visually separate for a reason: plugins, OCR subclasses, and the cloud converter attach at the same seam, meaning the library’s cloud story and its offline story are both “just converters.” The MCP package sits apart and small, wrapping the engine rather than duplicating any of it — the visual argument that clean core architecture makes the protocol layer trivial.
From Install to an Agent-Readable File
Installation is pip install markitdown, and the smallest useful command is markitdown report.pdf > report.md — point it at any supported file and markdown comes out stdout. From there the useful flags are few: -e lists which optional format extensions are enabled, --use-docintel routes PDFs through Azure Document Intelligence when you have an endpoint, and the LLM flags turn on image captioning when you have a vision-capable client. For agents, install the server package — pip install markitdown-mcp — and run markitdown-mcp for the stdio transport your MCP client expects, or add --sse for networked use; a Dockerfile ships for containerized deployments. Programmatically, MarkItDown().convert("file.docx").text_content is the whole API surface most applications need, and enable_plugins in the constructor is the switch that pulls third-party entry-point packages like the OCR one into the registry. Nothing in that flow requires configuration files, credentials, or a running service unless a specific converter — Document Intelligence, transcription, captioning — is used.
Honest limits: this is a converter, not a parser — complex PDF layouts, multi-column academic papers, and heavily designed decks lose fidelity, and the library is honest that its PDF story is text-first with visual extraction delegated to services. The web converters depend on third-party page structures, so YouTube and Bing handling can break when those sites change. OCR runs a local recognition model with its own dependency weight, and the MCP package, while tiny, inherits all of it — install only what you convert. And as with any preprocessor that feeds models, garbage in propagates: the tool’s own guidance is that output should be spot-checked before it becomes a retrieval corpus. Enjoyed this post? Never miss out on future posts by following us