# microsoft/markitdown — codebase unpack

> Canonical page: https://codevetter.com/unpack/markitdown

This is a deterministic Repo Unpack scan of microsoft/markitdown — a structural map, not a review, security audit, or maintainer-endorsed description.

- Scanned commit: b8f79c57ebc0044be41323d89b2a45d3fda8460e (2026-09-21)
- Last upstream commit: Bump versions or markitdown and markitdown-ocr (#2545)
- Generated by codevetter 1.12.0 on 2026-09-22
- 176 files scanned · 3.1 MB

## Languages

- Python: 106 files
- Markdown: 15 files
- TOML: 4 files
- YAML: 4 files
- HTML: 3 files
- JSON: 2 files

## Repository structure

- packages/ — 161 files
- .github/ — 4 files
- .devcontainer/ — 1 files

Probable source entrypoint: `packages/` (inferred from the tree, not upstream docs)

## Source layout — packages/

- packages/markitdown/ — 105 files
- packages/markitdown-mcp/ — 11 files
- packages/markitdown-ocr/ — 36 files
- packages/markitdown-sample-plugin/ — 9 files

## Package

- packages/markitdown-mcp/pyproject.toml: markitdown-mcp — 0 declared dependencies

## Structural health signals

Average score 7.7/10 across 52 files; 5 hotspots. These are deterministic review leads, not proof of a bug.

## Recent history

- b8f79c57ebc0 (2026-09-21) Bump versions or markitdown and markitdown-ocr (#2545)
- d51937de3aca (2026-09-21) fix: bump youtube-transcript-api to >=1.2.3 for Python 3.14 support (#2407)
- 945314a45ddb (2026-09-16) Refactor Office OCR converters to reuse core conversion pipelines (#2506)
- eb31b5c94536 (2026-09-14) bump_versions (#2490)
- cc0ca9edd8e2 (2026-09-12) Preserve whitepace underlines (#2477)

## Overview

MarkItDown is a Microsoft-maintained Python monorepo whose core package converts files, URLs, and byte streams into Markdown optimized for LLM consumption. It dispatches input through a priority-ordered registry of pluggable DocumentConverter classes after sniffing content with Google's magika model and charset-normalizer. The repo also ships an MCP (Model Context Protocol) server package exposing conversion as a tool, an official OCR plugin that uses LLM vision models, and a sample plugin demonstrating the entry-point-based plugin API.

## System map

The repo is a uv/pip-installable Python monorepo under packages/ with four distributions: markitdown (the library + `markitdown` CLI), markitdown-mcp (an MCP server wrapping the library), markitdown-ocr (a first-party plugin adding LLM-vision OCR), and markitdown-sample-plugin (an RTF reference plugin). The core engine is the MarkItDown class in _markitdown.py, which owns a requests.Session, a magika.Magika identifier, and a priority-sorted list of ConverterRegistration entries. All format logic lives in ~25 DocumentConverter subclasses under converters/ plus shared helpers in converter_utils/. Build is hatchling-based; tests run via `hatch test` or pytest in CI across Python 3.10-3.14 on Linux, Windows, and Windows ARM64.
- The central orchestrator is class MarkItDown in packages/markitdown/src/markitdown/_markitdown.py (line 141); its constructor builds a requests.Session with an 'Accept: text/markdown, text/html;q=0.9, ...' header and instantiates magika.Magika() for file-type identification. — [packages/markitdown/src/markitdown/_markitdown.py#L141-168](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L141-L168)
- Built-in converters are registered in enable_builtins() in a deliberate order: PlainTextConverter, ZipConverter, and HtmlConverter get PRIORITY_GENERIC_FILE_FORMAT (10.0) while ~15 specific converters get PRIORITY_SPECIFIC_FILE_FORMAT (0.0); lower values are tried first and registration order within a priority is reversed (insert at index 0). — [packages/markitdown/src/markitdown/_markitdown.py#L101-107](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L101-L107) [packages/markitdown/src/markitdown/_markitdown.py#L226-253](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L226-L253) [packages/markitdown/src/markitdown/_markitdown.py#L737-739](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L737-L739)
- Every converter implements the DocumentConverter interface defined in _base_converter.py: accepts(file_stream, stream_info, **kwargs) -> bool and convert(...) -> DocumentConverterResult; DocumentConverterResult carries `markdown` plus optional `title`, with `text_content` kept as a soft-deprecated alias. — [packages/markitdown/src/markitdown/_base_converter.py#L5-105](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_base_converter.py#L5-L105)
- Stream metadata flows through the frozen dataclass StreamInfo (mimetype, extension, charset, filename, local_path, url) with a copy_and_update() merge helper in _stream_info.py. — [packages/markitdown/src/markitdown/_stream_info.py#L5-32](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_stream_info.py#L5-L32)
- The CLI entrypoint is `markitdown = markitdown.__main__:main` declared in pyproject [project.scripts]; __main__.py builds an argparse parser supporting -o/--output, -x/--extension, -m/--mime-type, -c/--charset, -d/--use-docintel, --use-cu, -p/--use-plugins, --list-plugins, and --keep-data-uris. — [packages/markitdown/pyproject.toml#L74-75](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L74-L75) [packages/markitdown/src/markitdown/__main__.py#L51-145](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L51-L145)
- The MCP server package markitdown-mcp defines one tool `convert_to_markdown(uri)` on an MCPServer('markitdown') instance and can run over stdio (mcp.run()) or HTTP via a combined Starlette app merging the SSE and Streamable-HTTP sub-apps served by uvicorn on 127.0.0.1:3001 by default. — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L16-24](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L16-L24) [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L58-135](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L58-L135)
- Core runtime dependencies are deliberately thin: beautifulsoup4, requests, markdownify, magika~=0.6.1, charset-normalizer, defusedxml; all heavy parsers (mammoth, python-pptx, pandas/openpyxl/xlrd, pdfminer.six, pdfplumber, olefile, pydub, SpeechRecognition, youtube-transcript-api, Azure SDKs) are optional extras declared in pyproject.toml [project.optional-dependencies]. — [packages/markitdown/pyproject.toml#L27-64](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L27-L64)
- Third-party plugins are discovered via importlib.metadata entry_points(group='markitdown.plugin') in _load_plugins(); each plugin module must expose register_converters(markitdown, **kwargs), demonstrated by the sample plugin and the markitdown-ocr package's [project.entry-points."markitdown.plugin"] declaration. — [packages/markitdown/src/markitdown/_markitdown.py#L110-130](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L110-L130) [packages/markitdown-sample-plugin/src/markitdown_sample_plugin/_plugin.py#L25-31](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-sample-plugin/src/markitdown_sample_plugin/_plugin.py#L25-L31) [packages/markitdown-ocr/pyproject.toml#L55-57](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/pyproject.toml#L55-L57)
- The root Dockerfile builds a python:3.13-slim-trixie image installing ffmpeg and libimage-exiftool-perl, sets EXIFTOOL_PATH/FFMPEG_PATH/ORT_DISABLE_TELEMETRY env vars, pip-installs markitdown[all] plus the sample plugin, drops to USER nobody:nogroup, and uses ENTRYPOINT ['markitdown'] for stdin->stdout conversion. — [Dockerfile#L1-34](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/Dockerfile#L1-L34)
- CI runs three pytest/hatch jobs in .github/workflows/tests.yml (core hatch test matrix on ubuntu + windows + windows-11-vs2026-arm; ocr-tests installing both local packages; mcp-tests installing markitdown[all] + markitdown-mcp) plus a separate pre-commit workflow, with PIP_CONSTRAINT/UV_CONSTRAINT pinned to .github/constraints.txt. — [.github/workflows/tests.yml](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/.github/workflows/tests.yml) [.github/workflows/pre-commit.yml](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/.github/workflows/pre-commit.yml) [.github/constraints.txt](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/.github/constraints.txt)
- The public API surface is explicitly re-exported in markitdown/__init__.py: MarkItDown, DocumentConverter, DocumentConverterResult, StreamInfo, the exception hierarchy, and the two PRIORITY_* constants; converter classes are importable from markitdown.converters. — [packages/markitdown/src/markitdown/__init__.py#L5-34](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__init__.py#L5-L34) [packages/markitdown/src/markitdown/converters/__init__.py#L5-54](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/__init__.py#L5-L54)
- Versions are managed per-package via hatch's [tool.hatch.version] pointing at each package's __about__.py: markitdown 0.1.8, markitdown-mcp 0.0.1a7, markitdown-ocr 0.1.1. — [packages/markitdown/pyproject.toml#L71-72](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L71-L72) [packages/markitdown/src/markitdown/__about__.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__about__.py) [packages/markitdown-mcp/src/markitdown_mcp/__about__.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__about__.py) [packages/markitdown-ocr/src/markitdown_ocr/__about__.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/src/markitdown_ocr/__about__.py)

## Feature catalog

User-facing surface is three-fold: the `markitdown` CLI, the MarkItDown Python API (convert/convert_local/convert_stream/convert_uri/convert_response), and the `markitdown-mcp` server tool. Format coverage spans office docs (docx/xlsx/xls/pptx), PDF, images, audio, HTML plus URL-specialized converters (Wikipedia, YouTube, Bing SERP), EPUB, Outlook .msg, Jupyter notebooks, CSV, ZIP archives, and plain text. Cloud integrations offer Azure Document Intelligence and Azure Content Understanding as opt-in converters, and LLM-based image captions via a caller-supplied OpenAI-compatible client.
- The CLI accepts a filename or stdin (sys.stdin.buffer wrapped in BytesIO), writes markdown to stdout or -o file, and supports hint flags -x/-m/-c that populate a StreamInfo passed to convert(). — [packages/markitdown/src/markitdown/__main__.py#L251-263](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L251-L263) [packages/markitdown/src/markitdown/__main__.py#L180-188](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L180-L188)
- --use-docintel / -d requires -e/--endpoint or env var MARKITDOWN_DOCINTEL_ENDPOINT and registers DocumentIntelligenceConverter, which calls Azure's prebuilt-layout model via DocumentIntelligenceClient.begin_analyze_document with markdown output format. — [packages/markitdown/src/markitdown/__main__.py#L207-218](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L207-L218) [packages/markitdown/src/markitdown/converters/_doc_intel_converter.py#L241-258](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_doc_intel_converter.py#L241-L258)
- --use-cu requires --cu-endpoint or env MARKITDOWN_CU_ENDPOINT, accepts --cu-analyzer and --cu-file-types, and registers ContentUnderstandingConverter which supports documents, email, images, video and audio per the ContentUnderstandingFileType enum. — [packages/markitdown/src/markitdown/__main__.py#L219-247](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L219-L247) [packages/markitdown/src/markitdown/converters/_cu_converter.py#L55-98](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_cu_converter.py#L55-L98)
- --use-plugins / --list-plugins controls third-party plugin loading; plugins are disabled by default and discovered via the 'markitdown.plugin' entry-point group. — [packages/markitdown/src/markitdown/__main__.py#L126-137](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L126-L137) [packages/markitdown/src/markitdown/__main__.py#L190-205](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L190-L205)
- --keep-data-uris controls whether base64 data URIs are kept in output; default behavior truncates them via the keep_data_uris option of _CustomMarkdownify. — [packages/markitdown/src/markitdown/__main__.py#L139-143](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L139-L143) [packages/markitdown/src/markitdown/converters/_markdownify.py#L35-39](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_markdownify.py#L35-L39)
- DocxConverter subclasses HtmlConverter: it pre-processes the .docx zip (OMML math -> LaTeX via converter_utils/docx), merges caller style_map + embedded style map + a default 'u => u' underline rule, runs mammoth.convert_to_html, optionally adapts embedded images through an overridable _image_to_html hook, then converts the HTML via _CustomMarkdownify. — [packages/markitdown/src/markitdown/converters/_docx_converter.py#L43-143](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py#L43-L143) [packages/markitdown/src/markitdown/converter_utils/docx/pre_process.py#L34-80](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converter_utils/docx/pre_process.py#L34-L80)
- PdfConverter (589 lines) uses pdfminer.six and pdfplumber with helpers to emit aligned Markdown tables (_to_markdown_table), merge MasterFormat partial numbering, and extract form content; missing deps raise MissingDependencyException pointing at the [pdf] extra. — [packages/markitdown/src/markitdown/converters/_pdf_converter.py#L60-120](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_pdf_converter.py#L60-L120)
- XlsxConverter/XlsConverter share one module using pandas.read_excel with openpyxl/xlrd engines, including a repair path that renames the non-schema 'showZeroes' attribute inside <sheetView> start tags only. — [packages/markitdown/src/markitdown/converters/_xlsx_converter.py#L14-60](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_xlsx_converter.py#L14-L60)
- ImageConverter emits exiftool-extracted metadata fields (ImageSize, GPSPosition, etc.) and, when llm_client+llm_model kwargs are present, calls _get_llm_description which base64-embeds the image into an OpenAI chat.completions vision request. — [packages/markitdown/src/markitdown/converters/_image_converter.py#L39-138](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_image_converter.py#L39-L138)
- AudioConverter combines exiftool metadata with SpeechRecognition transcription (recognize_google) for wav/mp3/m4a/mp4 after pydub conversion to wav; missing pydub/SpeechRecognition raises MissingDependencyException for the [audio-transcription] extra. — [packages/markitdown/src/markitdown/converters/_audio_converter.py#L46-101](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_audio_converter.py#L46-L101) [packages/markitdown/src/markitdown/converters/_transcribe_audio.py#L29-58](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_transcribe_audio.py#L29-L58)
- YouTubeConverter accepts HTML streams whose stream_info.url parses to a youtu.be/youtube.com video id, scrapes metadata from the HTML, and optionally fetches transcripts via youtube-transcript-api; WikipediaConverter and BingSerpConverter similarly gate accepts() on the source URL. — [packages/markitdown/src/markitdown/converters/_youtube_converter.py#L41-80](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_youtube_converter.py#L41-L80) [packages/markitdown/src/markitdown/converters/_wikipedia_converter.py#L21-46](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_wikipedia_converter.py#L21-L46) [packages/markitdown/src/markitdown/converters/_bing_serp_converter.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_bing_serp_converter.py)
- EpubConverter opens the EPUB zip in-memory, parses META-INF/container.xml and content.opf with defusedxml.minidom, extracts Dublin Core metadata, resolves spine hrefs (with percent-decoding fallback), and converts each spine HTML file through HtmlConverter. — [packages/markitdown/src/markitdown/converters/_epub_converter.py#L55-134](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_epub_converter.py#L55-L134)
- OutlookMsgConverter parses .msg OLE files with olefile, including manual decoding of the __properties_version1.0 stream and a CODEPAGE_CODECS table mapping ~30 Windows code page IDs to Python codecs for ANSI-encoded bodies. — [packages/markitdown/src/markitdown/converters/_outlook_msg_converter.py#L21-75](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_outlook_msg_converter.py#L21-L75)
- ZipConverter recursively converts each archive member by calling self._markitdown.convert_stream on the decompressed bytes, prefixing each result with '## File: <name>' and silently skipping members that raise UnsupportedFormatException or FileConversionException. — [packages/markitdown/src/markitdown/converters/_zip_converter.py#L87-130](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_zip_converter.py#L87-L130)
- The markitdown-mcp package exposes a single MCP tool convert_to_markdown(uri) that accepts http:, https:, file: and data: URIs and returns markdown text, mapping internal exceptions to sanitized ToolError messages. — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-47](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-L47)
- markitdown-ocr ships four enhanced converters (PdfConverterWithOCR, DocxConverterWithOCR, PptxConverterWithOCR, XlsxConverterWithOCR) that extract embedded images and OCR them via an LLMVisionOCRService wrapping an OpenAI-compatible chat.completions call. — [packages/markitdown-ocr/src/markitdown_ocr/_plugin.py#L49-68](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/src/markitdown_ocr/_plugin.py#L49-L68) [packages/markitdown-ocr/src/markitdown_ocr/_ocr_service.py#L24-111](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/src/markitdown_ocr/_ocr_service.py#L24-L111) [packages/markitdown-ocr/src/markitdown_ocr/_pdf_converter_with_ocr.py#L1-50](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/src/markitdown_ocr/_pdf_converter_with_ocr.py#L1-L50)

## Data flow

Data enters through convert()/convert_local()/convert_stream()/convert_uri()/convert_response(), is normalized to a seekable BinaryIO plus a list of StreamInfo guesses, then fanned out to priority-sorted converters. Type detection is layered: caller hints and headers produce a base StreamInfo, mimetypes fills gaps, then magika identifies content from bytes and charset-normalizer guesses text encoding; incompatible guesses produce two candidate StreamInfos tried in order. The winning converter returns a DocumentConverterResult whose markdown is normalized (trailing whitespace stripped, 3+ newlines collapsed) before returning.
- convert() is a type-based dispatcher: str with scheme http/https/file/data routes to convert_uri, other strings and Path to convert_local, requests.Response to convert_response, and readable non-TextIO objects to convert_stream; anything else raises TypeError. — [packages/markitdown/src/markitdown/_markitdown.py#L322-366](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L322-L366)
- convert_local() seeds StreamInfo with local_path, os.path.splitext extension, and basename, merges caller stream_info (plus deprecated file_extension/url kwargs), opens the file 'rb', guesses stream info, then calls _convert. — [packages/markitdown/src/markitdown/_markitdown.py#L368-403](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L368-L403)
- convert_stream() buffers non-seekable streams fully into io.BytesIO (4 KiB chunks) before guessing — the entire file is resident in memory from that point on. — [packages/markitdown/src/markitdown/_markitdown.py#L435-450](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L435-L450)
- convert_uri() branches by scheme: file: URIs are resolved by file_uri_to_path (rejecting non-localhost netlocs and UNC/device paths), data: URIs are decoded by parse_data_uri (base64 or percent), and http(s): is fetched with self._requests_session.get(uri, stream=True) then converted via convert_response. — [packages/markitdown/src/markitdown/_markitdown.py#L471-531](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L471-L531) [packages/markitdown/src/markitdown/_uri_utils.py#L15-75](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_uri_utils.py#L15-L75)
- convert_response() builds the base StreamInfo from Content-Type (mimetype + charset), Content-Disposition filename (parsed via email.message.Message, RFC 2231-aware), or the URL path extension, then buffers the whole body via response.iter_content(chunk_size=512) into BytesIO. — [packages/markitdown/src/markitdown/_markitdown.py#L533-604](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L533-L604) [packages/markitdown/src/markitdown/_markitdown.py#L56-70](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L56-L70)
- _get_stream_info_guesses() enhances the base guess via the mimetypes module, then calls magika.identify_stream; for text predictions it samples up to 64 KiB (completing a split trailing UTF-8 char via _read_charset_sample) and runs charset_normalizer.from_bytes().best() for charset detection. — [packages/markitdown/src/markitdown/_markitdown.py#L741-840](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L741-L840) [packages/markitdown/src/markitdown/_markitdown.py#L73-98](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L73-L98)
- When magika's prediction is incompatible with caller-supplied mimetype/extension/charset, both the enhanced base guess and the magika guess are appended as separate StreamInfo candidates, each tried in sequence against all converters in _convert. — [packages/markitdown/src/markitdown/_markitdown.py#L787-833](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L787-L833) [packages/markitdown/src/markitdown/_markitdown.py#L622](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L622)
- _convert() iterates 'stream_info_guesses + [StreamInfo()]' outer loop and priority-sorted converters inner loop, asserting the stream position is unchanged by accepts(); on convert() success it normalizes text_content (rstrip each line, collapse >=3 newlines to 2) and returns immediately. — [packages/markitdown/src/markitdown/_markitdown.py#L606-690](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L606-L690)
- Per-call kwargs are enriched in _convert with global llm_client/llm_model/llm_prompt/style_map/exiftool_path, a private _parent_converters list for nested processing, and legacy file_extension/url keys derived from the current StreamInfo guess. — [packages/markitdown/src/markitdown/_markitdown.py#L630-657](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L630-L657)
- Converters signal outcomes via exceptions: convert() failures are collected as FailedConversionAttempt and, if no converter succeeds, raised as FileConversionException (with per-converter trace summary); if no converter even accepted, UnsupportedFormatException is raised. — [packages/markitdown/src/markitdown/_markitdown.py#L671-699](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L671-L699) [packages/markitdown/src/markitdown/_exceptions.py#L42-82](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_exceptions.py#L42-L82)
- Office/HTML converters converge on HTML as an intermediate: DocxConverter (mammoth), EpubConverter (spine files), PptxConverter and RSS/Wikipedia converters produce HTML that is finalized by HtmlConverter -> _CustomMarkdownify, which also strips javascript links and truncates data URIs unless keep_data_uris is set. — [packages/markitdown/src/markitdown/converters/_docx_converter.py#L113-123](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py#L113-L123) [packages/markitdown/src/markitdown/converters/_html_converter.py#L42-91](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_html_converter.py#L42-L91) [packages/markitdown/src/markitdown/converters/_markdownify.py#L25-120](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_markdownify.py#L25-L120)
- In the MCP server, each convert_to_markdown call constructs a fresh MarkItDown instance (plugins gated by env MARKITDOWN_ENABLE_PLUGINS) and calls convert_uri; every exception type is mapped to a ToolError that deliberately omits internal detail (e.g., FileConversionException becomes 'File conversion failed.', OSError reports only os.strerror). — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-55](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-L55)

## Behavior traces

Three load-bearing flows were traced end-to-end. (1) CLI file conversion: argparse -> MarkItDown() -> convert -> convert_local -> _get_stream_info_guesses -> _convert -> _handle_output. (2) HTTP/MCP conversion: convert_uri -> requests GET -> convert_response -> _convert, with the MCP tool wrapping it in sanitized ToolErrors. (3) Plugin registration + nested zip/docx conversion showing the recursive and registry-driven design.
- CLI local-file flow: main() parses args -> normalizes extension/mime/charset hints into StreamInfo -> constructs MarkItDown(enable_plugins=args.use_plugins) -> markitdown.convert(filename) -> convert_local -> open(path,'rb') -> _get_stream_info_guesses (mimetypes -> magika -> charset_normalizer) -> _convert -> _handle_output writes markdown to -o file or stdout with errors='replace' encoding. — [packages/markitdown/src/markitdown/__main__.py#L148-263](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L148-L263) [packages/markitdown/src/markitdown/_markitdown.py#L368-403](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L368-L403)
- Inside _convert, converters are tried in (guess, converter) order; each converter's accepts() runs first under a stream-position assertion, then convert() runs inside try/finally that always seeks the stream back to cur_pos — so every converter sees the stream from the same offset. — [packages/markitdown/src/markitdown/_markitdown.py#L619-690](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L619-L690)
- HTTP fetch flow: convert_uri('https://...') -> self._requests_session.get(uri, stream=True) -> raise_for_status -> convert_response parses content-type/content-disposition headers -> iter_content buffering -> _get_stream_info_guesses -> _convert; the default session sends 'Accept: text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.1'. — [packages/markitdown/src/markitdown/_markitdown.py#L517-527](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L517-L527) [packages/markitdown/src/markitdown/_markitdown.py#L533-604](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L533-L604) [packages/markitdown/src/markitdown/_markitdown.py#L155-166](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L155-L166)
- MCP tool flow: client calls tools/call convert_to_markdown{uri} -> handler creates MarkItDown(enable_plugins=check_plugins_enabled()) -> convert_uri -> on success returns result.markdown; on failure the except chain converts UnsupportedFormatException/HTTPError/RequestException/FileConversionException/OSError/ValueError into ToolError messages. — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-55](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-L55)
- MCP HTTP startup: main() parses --http/--sse/--host/--port -> for non-localhost hosts prints a security warning to stderr -> create_starlette_app mounts mcp.sse_app() and mcp.streamable_http_app(json_response=True, stateless_http=True) routes under a combined lifespan -> uvicorn.run(app, host, port or 3001). — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L58-135](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L58-L135)
- Plugin activation flow: MarkItDown(enable_plugins=True) -> enable_plugins() -> _load_plugins() iterates entry_points(group='markitdown.plugin'), entry_point.load() with per-plugin try/except+warn -> plugin.register_converters(self, **kwargs) -> each converter lands in self._converters via register_converter(priority). — [packages/markitdown/src/markitdown/_markitdown.py#L110-130](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L110-L130) [packages/markitdown/src/markitdown/_markitdown.py#L302-320](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L302-L320) [packages/markitdown-ocr/src/markitdown_ocr/_plugin.py#L19-68](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/src/markitdown_ocr/_plugin.py#L19-L68)
- ZIP recursion flow: ZipConverter.convert opens zipfile.ZipFile on the stream -> for each entry reads bytes into BytesIO + a fresh StreamInfo(extension, filename) -> calls self._markitdown.convert_stream (the same MarkItDown instance passed at construction) -> concatenates results under '## File:' headings; failures are swallowed per-member. — [packages/markitdown/src/markitdown/converters/_zip_converter.py#L61-130](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_zip_converter.py#L61-L130)
- DOCX conversion flow: DocxConverter.convert -> dependency check raising MissingDependencyException -> pre_process_docx (rewrites document.xml: OMML oMath -> LaTeX $...$/$...$ w:t replacements) -> mammoth.convert_to_html with merged style_map -> optional _DocxImages adapter when _image_to_html is overridden -> HtmlConverter.convert_string -> _CustomMarkdownify. — [packages/markitdown/src/markitdown/converters/_docx_converter.py#L70-143](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py#L70-L143) [packages/markitdown/src/markitdown/converter_utils/docx/pre_process.py#L34-80](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converter_utils/docx/pre_process.py#L34-L80)
- EPUB conversion flow: EpubConverter.convert -> zipfile -> defusedxml.minidom.parse('META-INF/container.xml') -> locate content.opf -> extract dc:* metadata and manifest/spine -> _resolve_manifest_href per spine item -> HtmlConverter.convert on each zip member stream -> metadata block prepended, title set on result. — [packages/markitdown/src/markitdown/converters/_epub_converter.py#L55-134](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_epub_converter.py#L55-L134)
- Stdin flow: when no filename is given, main() wraps sys.stdin.buffer.read() in io.BytesIO (because Windows pipe-backed stdin can falsely report seekable()) and calls convert_stream with the hint StreamInfo and keep_data_uris flag. — [packages/markitdown/src/markitdown/__main__.py#L251-257](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__main__.py#L251-L257) [packages/markitdown/src/markitdown/_markitdown.py#L435-444](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L435-L444)
- LLM image-caption flow: ImageConverter.convert -> exiftool_metadata (subprocess exiftool -json -, version-gated >=12.24 for CVE-2021-22204) -> if llm_client+llm_model kwargs present, _get_llm_description base64-encodes the stream into a data URI and calls client.chat.completions.create(model=..., messages=[...image_url...]). — [packages/markitdown/src/markitdown/converters/_image_converter.py#L46-138](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_image_converter.py#L46-L138) [packages/markitdown/src/markitdown/converters/_exiftool.py#L10-57](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_exiftool.py#L10-L57) [packages/markitdown/src/markitdown/converters/_llm_caption.py#L7-50](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_llm_caption.py#L7-L50)

## Testing signals

Tests are pytest-based and live in packages/*/tests. The core package has ~35 test modules anchored by a declarative FileTestVector harness (_test_vectors.py) asserting must_include/must_not_include substrings across 20+ real fixture files in tests/test_files. Coverage spans stream-info guessing, CLI vectors, per-converter edge cases (PDF tables/memory, docx math/images/styles, pptx images/SVG/notes, xlsx images, RSS titles, Outlook ANSI codepages, charset edge cases) and URI handling; remote-URL tests are skipped under GITHUB_ACTIONS. MCP tests drive the real stdio server subprocess and an in-process Starlette app via httpx.
- packages/markitdown/tests/_test_vectors.py defines FileTestVector(filename, mimetype, charset, url, must_include, must_not_include) and two vector sets (GENERAL_TEST_VECTORS with 16 entries, DATA_URI_TEST_VECTORS) that parametrized tests consume. — [packages/markitdown/tests/_test_vectors.py#L5-279](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/_test_vectors.py#L5-L279)
- test_module_vectors.py parametrizes every vector across test_guess_stream_info, test_convert_local, test_convert_stream_with_hints and more; tests needing live URLs are gated by skip_remote = bool(os.environ.get('GITHUB_ACTIONS')). — [packages/markitdown/tests/test_module_vectors.py#L21-80](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_module_vectors.py#L21-L80)
- Fixture corpus is real binary files under packages/markitdown/tests/test_files (docx/xlsx/xls/pptx/pdf/msg/epub/zip/html/csv/json/ipynb/jpg/mp3/m4a/wav and edge cases like random.bin, medical-report scans, borderless-table PDFs). — [packages/markitdown/tests/test_files](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_files)
- Edge-case suites are organized per concern: test_pdf_tables.py (1198 lines), test_cu_converter.py (1035 lines), test_pdf_memory.py, test_pdf_masterformat.py, test_docx_math*.py, test_docx_styles.py, test_pptx_images.py, test_xlsx_images.py, test_outlook_msg_ansi.py, test_charset_sample.py, test_undetectable_charset.py, test_csv_line_endings.py, test_csv_blank_runs.py, test_file_paths.py, test_zip_kwargs.py. — [packages/markitdown/tests](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests)
- URI/URL parsing is tested in test_module_misc.py: data: URIs (base64, charset attributes, missing mime), file:// URIs including localhost netloc, percent-encoded Windows drive letters, and case-insensitive schemes. — [packages/markitdown/tests/test_module_misc.py#L250-380](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_module_misc.py#L250-L380)
- MCP tests include test_stdio_protocols.py which spawns `python -m markitdown_mcp` as a subprocess and feeds newline-delimited JSON-RPC covering both the legacy initialize handshake and a 2026-07-28 server/discover request, plus test_http_transports.py exercising create_starlette_app in-process with httpx, and test_tool_errors.py for error mapping. — [packages/markitdown-mcp/tests/test_stdio_protocols.py#L1-60](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/tests/test_stdio_protocols.py#L1-L60) [packages/markitdown-mcp/tests/test_http_transports.py#L1-40](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/tests/test_http_transports.py#L1-L40)
- markitdown-ocr tests cover each enhanced converter (test_pdf/docx/pptx/xlsx_converter.py), inheritance behavior proving the OCR variants subclass the built-ins, and OCR metadata propagation, with fixtures under tests/ocr_test_data. — [packages/markitdown-ocr/tests](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/tests)
- CI matrix: tests.yml runs hatch test for packages/markitdown on Python 3.10-3.14 (ubuntu) plus 3.12 on windows-latest and windows-11-vs2026-arm; separate jobs pip-install local packages and run pytest for markitdown-ocr and markitdown-mcp; PYTHONUTF8=1 is set for captured CLI output. — [.github/workflows/tests.yml](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/.github/workflows/tests.yml)
- Coverage config lives in each pyproject ([tool.coverage.run] branch=true, source_pkgs covering src + tests) and hatch envs define `hatch test` (hatch-test env with openai extra dep) and a types env running `mypy --ignore-missing-imports` over src and tests. — [packages/markitdown/pyproject.toml#L77-113](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L77-L113)
- There are no unit tests for the CLI's Azure paths or plugin loading mechanics in the core package, and remote-URL vectors (YouTube/Wikipedia/Bing) are skipped in CI — those converter paths are only exercised by local HTML fixtures. (inferred) — [packages/markitdown/tests/test_module_vectors.py#L21-23](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_module_vectors.py#L21-L23) [packages/markitdown/tests/_test_vectors.py#L111-140](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/_test_vectors.py#L111-L140)

## Risk map

The dominant risk theme is I/O breadth: the library deliberately performs filesystem, network, and subprocess I/O with the caller's privileges, and the README explicitly warns callers to sanitize inputs. Specific hotspots: unrestricted HTTP fetching (SSRF) via convert_uri, file:// URI local file reads, unbounded in-memory buffering of remote/stream content, subprocess invocation of exiftool, plugin code execution via entry points, and an unauthenticated MCP HTTP mode. Defusedxml and an exiftool version check show some hardening is present.
- convert_uri fetches arbitrary http(s) URLs with the process's requests session and no allowlist — the README's Security Considerations section tells integrators to block private/loopback/link-local/metadata-service addresses themselves; nothing in code enforces this. — [packages/markitdown/src/markitdown/_markitdown.py#L517-527](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L517-L527) [README.md#L394-400](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/README.md#L394-L400)
- file:// URIs resolve to local paths and convert_local opens them; file_uri_to_path rejects non-localhost netlocs and UNC/device paths but permits any absolute local path — the MCP tool exposes this to remote clients in HTTP mode. — [packages/markitdown/src/markitdown/_uri_utils.py#L15-39](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_uri_utils.py#L15-L39) [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-24](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L19-L24)
- The MCP server's --http mode has NO authentication; the code itself prints a warning when --host is non-loopback stating any reachable user 'can read files and fetch network resources accessible to this user'. — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L114-127](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L114-L127)
- Remote and stream inputs are buffered fully into memory: convert_response iter_content into BytesIO and convert_stream buffers non-seekable streams — a hostile or huge response/stream can exhaust memory. — [packages/markitdown/src/markitdown/_markitdown.py#L594-598](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L594-L598) [packages/markitdown/src/markitdown/_markitdown.py#L435-444](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L435-L444)
- exiftool_metadata shells out to subprocess.run([exiftool_path, ...]); the exiftool path is resolved from the EXIFTOOL_PATH env var, kwargs, or a PATH search restricted to well-known directories, and a version gate rejects exiftool <12.24 citing CVE-2021-22204. — [packages/markitdown/src/markitdown/converters/_exiftool.py#L20-47](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_exiftool.py#L20-L47) [packages/markitdown/src/markitdown/_markitdown.py#L202-224](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L202-L224)
- Plugin loading executes arbitrary installed Python code: entry_points(group='markitdown.plugin').load() then register_converters(); failures are only warned about. Plugins are off by default but -p/--use-plugins or enable_plugins=True activates all of them. — [packages/markitdown/src/markitdown/_markitdown.py#L113-130](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L113-L130) [packages/markitdown/src/markitdown/_markitdown.py#L302-320](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L302-L320)
- The _convert loop asserts (not raises) on stream-position invariants ('assert cur_pos == file_stream.tell()') — running under python -O disables these checks, silently removing the safety net that protects converters from a misbehaving accepts(). — [packages/markitdown/src/markitdown/_markitdown.py#L625-669](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L625-L669)
- Each MCP convert_to_markdown call constructs a brand-new MarkItDown() — re-registering ~17 converters and re-instantiating magika.Magika() (a model load) per request, which is a latency/CPU cost on every tool call. (inferred) — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L22](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L22) [packages/markitdown/src/markitdown/_markitdown.py#L168](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L168)
- BeautifulSoup 'html.parser' and defusedxml are used for untrusted markup/XML (EPUB container/OPF via defusedxml.minidom), but docx pre-processing parses document.xml with BeautifulSoup and xml.etree.ElementTree — XML bomb surface depends on those parsers' defaults rather than defusedxml. (inferred) — [packages/markitdown/src/markitdown/converters/_epub_converter.py#L5-71](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_epub_converter.py#L5-L71) [packages/markitdown/src/markitdown/converter_utils/docx/pre_process.py#L1-10](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converter_utils/docx/pre_process.py#L1-L10)
- HtmlConverter's markdownify path can hit RecursionError on deeply nested HTML; it falls back to get_text() unless a 'strict' kwarg is set — acceptable degradation but callers get silently different output shape. — [packages/markitdown/src/markitdown/converters/_html_converter.py#L68-81](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_html_converter.py#L68-L81)
- ZipConverter swallows FileConversionException/UnsupportedFormatException per member and the archive is read via zipObj.read(entry) fully into memory per entry; nested zips recurse through the same converter stack without a depth limit visible in this file. — [packages/markitdown/src/markitdown/converters/_zip_converter.py#L108-128](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_zip_converter.py#L108-L128)
- Error detail asymmetry: FileConversionException aggregates per-converter exception messages (which can include SDK/internal detail) while the MCP layer deliberately sanitizes to generic ToolError strings — good for the MCP boundary, but library callers receive verbose internals. — [packages/markitdown/src/markitdown/_exceptions.py#L52-82](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_exceptions.py#L52-L82) [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L27-47](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L27-L47)

## Extension points

MarkItDown is explicitly designed for extension at two levels: in-process registration via MarkItDown.register_converter(converter, priority=...) and distribution-level plugins via the 'markitdown.plugin' entry-point group. Converters subclass DocumentConverter and implement accepts()/convert(); several built-ins expose finer hooks (DocxConverter._image_to_html, style_map, llm_* kwargs). The markitdown-ocr package is the canonical example of a plugin that overrides built-ins by registering with negative priority.
- register_converter(converter, *, priority=PRIORITY_SPECIFIC_FILE_FORMAT) inserts a ConverterRegistration at index 0 of self._converters; docstring documents that equal priorities preserve registration order with most-recent-first, and that plugins can pick priorities (e.g., 9) to slot between specific and generic converters. — [packages/markitdown/src/markitdown/_markitdown.py#L709-739](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L709-L739)
- Plugin contract: a package declares [project.entry-points."markitdown.plugin"] name = 'module', the module defines __plugin_interface_version__ = 1 and register_converters(markitdown, **kwargs) which calls markitdown.register_converter(...). — [packages/markitdown-sample-plugin/pyproject.toml](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-sample-plugin/pyproject.toml) [packages/markitdown-sample-plugin/src/markitdown_sample_plugin/_plugin.py#L13-31](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-sample-plugin/src/markitdown_sample_plugin/_plugin.py#L13-L31)
- markitdown-ocr demonstrates priority-based replacement: it registers its four converters at priority -1.0 so they run before all built-ins at 0.0, and its *WithOCR classes subclass the built-in converters so inherits tests verify drop-in compatibility. — [packages/markitdown-ocr/src/markitdown_ocr/_plugin.py#L49-68](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/src/markitdown_ocr/_plugin.py#L49-L68) [packages/markitdown-ocr/tests/test_docx_inheritance.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-ocr/tests/test_docx_inheritance.py)
- New converters plug in by subclassing DocumentConverter and implementing accepts(file_stream, stream_info, **kwargs)->bool and convert(...)->DocumentConverterResult; the docstrings document that accepts() must restore the stream position if it reads. — [packages/markitdown/src/markitdown/_base_converter.py#L42-105](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_base_converter.py#L42-L105)
- DocxConverter._image_to_html is an overridable hook: if a subclass overrides it, convert() wires a _DocxImages adapter into mammoth's convert_image callback so subclasses can render embedded images as custom HTML fragments. — [packages/markitdown/src/markitdown/converters/_docx_converter.py#L105-143](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py#L105-L143)
- Global kwargs llm_client, llm_model, llm_prompt, style_map, and exiftool_path are injected into every converter call by _convert(), letting callers extend behavior (e.g., captions, custom docx style maps) without subclassing. — [packages/markitdown/src/markitdown/_markitdown.py#L630-657](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L630-L657)
- _parent_converters is passed through kwargs so container converters (ZipConverter holds a MarkItDown reference; epub/docx delegate to HtmlConverter) can recurse with the full registry — a nested-conversion extension channel. — [packages/markitdown/src/markitdown/_markitdown.py#L648-649](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L648-L649) [packages/markitdown/src/markitdown/converters/_zip_converter.py#L61-67](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_zip_converter.py#L61-L67)
- _CustomMarkdownify subclasses markdownify.MarkdownConverter, so any markdownify option can be passed through kwargs from converters that emit HTML (HtmlConverter, DocxConverter, EpubConverter, RssConverter, etc.). — [packages/markitdown/src/markitdown/converters/_markdownify.py#L25-39](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_markdownify.py#L25-L39) [packages/markitdown/src/markitdown/converters/_html_converter.py#L93-110](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_html_converter.py#L93-L110)
- Cloud converters are opt-in registrations: passing docintel_endpoint/cu_endpoint (+credential/file_types/analyzer_id kwargs) to MarkItDown() registers DocumentIntelligenceConverter/ContentUnderstandingConverter at default priority, effectively a DI-based extension point. — [packages/markitdown/src/markitdown/_markitdown.py#L254-296](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L254-L296)
- register_page_converter() remains as a deprecated alias emitting DeprecationWarning, showing the registration API is the stable public seam. — [packages/markitdown/src/markitdown/_markitdown.py#L701-707](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L701-L707)

## Contributor handoff

Edit surface is small and regular: to add/change a format, touch one file in converters/ plus tests. The invariants that matter: accepts() must leave the stream position unchanged; converters must raise MissingDependencyException (not ImportError) for optional deps; registration order is reversed within a priority. Run tests per-package (hatch test or pytest). Avoid touching _markitdown.py's _convert loop casually — many subtle contracts (position asserts, guess iteration, legacy kwargs) live there.
- To add a format: create converters/_foo_converter.py with ACCEPTED_MIME_TYPE_PREFIXES/ACCEPTED_FILE_EXTENSIONS constants and a DocumentConverter subclass, export it in converters/__init__.py, register it in MarkItDown.enable_builtins at the right spot, and add the extra to pyproject [project.optional-dependencies]. — [packages/markitdown/src/markitdown/converters/__init__.py#L5-54](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/__init__.py#L5-L54) [packages/markitdown/src/markitdown/_markitdown.py#L229-253](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L229-L253) [packages/markitdown/pyproject.toml#L36-64](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L36-L64)
- Optional-dependency convention: wrap the import in try/except ImportError storing sys.exc_info() in a module-level _dependency_exc_info, then raise MissingDependencyException(MISSING_DEPENDENCY_MESSAGE.format(converter=..., extension=..., feature=...)) inside convert(). — [packages/markitdown/src/markitdown/converters/_docx_converter.py#L14-22](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py#L14-L22) [packages/markitdown/src/markitdown/converters/_docx_converter.py#L76-88](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py#L76-L88) [packages/markitdown/src/markitdown/_exceptions.py#L3-8](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_exceptions.py#L3-L8)
- Hard invariant from _base_converter docstring and _convert asserts: if accepts() reads from file_stream it MUST seek back; a converter that violates this triggers AssertionError inside _convert. — [packages/markitdown/src/markitdown/_base_converter.py#L62-70](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_base_converter.py#L62-L70) [packages/markitdown/src/markitdown/_markitdown.py#L666-669](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L666-L669)
- Registration order is counterintuitive: register_converter inserts at index 0, so LATER registrations are tried FIRST among equal priority — the comment in enable_builtins warns 'the most specific converters should appear below the most generic converters' in source order. — [packages/markitdown/src/markitdown/_markitdown.py#L226-228](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L226-L228) [packages/markitdown/src/markitdown/_markitdown.py#L737-739](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L737-L739)
- Test workflow: cd packages/markitdown && pip install hatch && hatch test (hatch env auto-installs [all] + openai); or run pytest directly after installing 'markitdown[all]' editable. CI also runs per-package pytest for markitdown-ocr and markitdown-mcp. — [packages/markitdown/pyproject.toml#L77-94](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L77-L94) [.github/workflows/tests.yml](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/.github/workflows/tests.yml) [README.md#L369-392](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/README.md#L369-L392)
- New test-vector files go in tests/test_files plus a FileTestVector entry in tests/_test_vectors.py; expected-output assertions are substring-based (must_include/must_not_include), not snapshot diffs. — [packages/markitdown/tests/_test_vectors.py#L5-33](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/_test_vectors.py#L5-L33) [packages/markitdown/tests/test_module_vectors.py#L59-70](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_module_vectors.py#L59-L70)
- Deprecated kwargs file_extension/url still flow through convert_local/convert_stream/convert_response and _convert injects them from stream_info — preserve them when refactoring signatures or tests like test_zip_kwargs.py will break. — [packages/markitdown/src/markitdown/_markitdown.py#L368-403](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L368-L403) [packages/markitdown/src/markitdown/_markitdown.py#L651-657](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py#L651-L657) [packages/markitdown/tests/test_zip_kwargs.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_zip_kwargs.py)
- MCP error contract: do not let raw exceptions leak — convert_to_markdown deliberately maps each exception class to a sanitized ToolError; new exception types from the library need explicit handling there. — [packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L26-47](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/src/markitdown_mcp/__main__.py#L26-L47) [packages/markitdown-mcp/tests/test_tool_errors.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown-mcp/tests/test_tool_errors.py)
- Read first: _markitdown.py (engine), _base_converter.py + _stream_info.py (contracts), one representative converter (_docx_converter.py for the optional-dep + HTML-intermediate pattern), and _markdownify.py for output shaping; for plugin work, _plugin.py in both markitdown-ocr and markitdown-sample-plugin. (inferred) — [packages/markitdown/src/markitdown/_markitdown.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_markitdown.py) [packages/markitdown/src/markitdown/_base_converter.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/_base_converter.py) [packages/markitdown/src/markitdown/converters/_docx_converter.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/converters/_docx_converter.py)
- Known trap: tests/test_module_vectors.py sets skip_remote only on GITHUB_ACTIONS — local runs may hit raw.githubusercontent.com for some vectors, so sandboxed agents should set GITHUB_ACTIONS=1 or expect network tests. — [packages/markitdown/tests/test_module_vectors.py#L21-26](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/tests/test_module_vectors.py#L21-L26)
- Version bumps happen per package in each src/<pkg>/__about__.py via hatch dynamic versioning — there is no root-level version file. — [packages/markitdown/pyproject.toml#L71-72](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/pyproject.toml#L71-L72) [packages/markitdown/src/markitdown/__about__.py](https://github.com/microsoft/markitdown/blob/b8f79c57ebc0044be41323d89b2a45d3fda8460e/packages/markitdown/src/markitdown/__about__.py)

Source repository: https://github.com/microsoft/markitdown
Full evidence corpus: https://github.com/Codevetter/codevetter/tree/main/benchmarks/repo-unpacks/markitdown.json

## Public product links

- [CodeVetter](https://codevetter.com/)
- [Download](https://codevetter.com/download)
- [Documentation](https://codevetter.com/docs/)
- [Source](https://github.com/Codevetter/codevetter)
