1. Architecture Overview
MarkItDown is a Python library and CLI tool for converting documents to Markdown, designed for LLM and text analysis pipelines. Built by Microsoft's AutoGen team, it follows a modular converter-chain architecture where file detection, format matching, and conversion are cleanly separated.
The system operates as a pipeline: an input (file path, URL, binary stream, or requests.Response) enters the MarkItDown class, which detects the file type through multiple strategies, then iterates through registered converters in priority order until one successfully produces a DocumentConverterResult containing Markdown.
Architecture Diagram
MarkItDown
┌──────────────────────┐
Input ──────────► │ convert() │
(path/URL/stream) │ │ │
│ ▼ │
│ _get_stream_info │
│ _guesses() │
│ │ magika │
│ │ mimetypes │
│ │ charset_norm │
│ ▼ │
│ _convert() │
│ │ │
│ ▼ │
│ Converter Chain │──► DocumentConverterResult
│ (priority-sorted) │ .markdown
│ ├─ DocIntel -1.0 │ .title
│ ├─ PDF 0.0 │
│ ├─ DOCX 0.0 │
│ ├─ XLSX 0.0 │
│ ├─ ... 0.0 │
│ ├─ HTML 10.0 │
│ ├─ ZIP 10.0 │
│ └─ PlainText 10.0 │
└──────────────────────┘
Plugin System (entry_points "markitdown.plugin")
┌──────────────────┐
│ markitdown-ocr │──► PdfConverterWithOCR (-1.0)
│ │──► DocxConverterWithOCR (-1.0)
│ │──► PptxConverterWithOCR (-1.0)
│ │──► XlsxConverterWithOCR (-1.0)
└──────────────────┘
MCP Server (markitdown-mcp)
┌──────────────────┐
│ FastMCP │──► STDIO / Streamable HTTP / SSE
│ convert_to_ │
│ markdown(uri) │──► MarkItDown.convert_uri()
└──────────────────┘
Note
The converter chain uses a stable sort by priority. Within the same priority level, most recently registered converters are tried first (LIFO). This means plugin converters registered at priority
-1.0always run before built-in converters at0.0, effectively overriding them.
Component Inventory
| Component | Package | Path | Role |
|---|---|---|---|
| Core library | markitdown |
packages/markitdown/ |
File detection, converter chain, Python API, CLI |
| MCP server | markitdown-mcp |
packages/markitdown-mcp/ |
Model Context Protocol server for LLM integration |
| OCR plugin | markitdown-ocr |
packages/markitdown-ocr/ |
LLM Vision-based OCR for PDF, DOCX, PPTX, XLSX |
| Sample plugin | markitdown-sample-plugin |
packages/markitdown-sample-plugin/ |
Reference implementation for third-party plugins (RTF converter) |
Technology Stack
| Layer | Technology | Version Constraint |
|---|---|---|
| Language | Python | >= 3.10 |
| Build system | Hatch / Hatchling | Latest |
| File detection | Google Magika | ~= 0.6.1 |
| HTML parsing | BeautifulSoup4 | Latest |
| HTML-to-Markdown | markdownify | Latest |
| PDF extraction | pdfminer.six | >= 20251230 |
| PDF tables | pdfplumber | >= 0.11.9 |
| DOCX conversion | mammoth | ~= 1.11.0 |
| PPTX handling | python-pptx | Latest |
| Excel | pandas + openpyxl / xlrd | Latest |
| HTTP | requests | Latest |
| XML security | defusedxml | Latest |
| Charset detection | charset-normalizer | Latest |
| MCP protocol | mcp | ~= 1.8.0 |
| MCP HTTP server | Starlette + uvicorn | Latest |
| Azure Document Intelligence | azure-ai-documentintelligence | Latest |
2. System Requirements
| Resource | Minimum | Notes |
|---|---|---|
| Python | 3.10 | Tested on 3.10, 3.11, 3.12, 3.13; CPython and PyPy |
| RAM | 256 MB | Large PDFs or batch processing may require more |
| Disk | Minimal | No persistent storage; all conversion is in-memory |
| Network | Optional | Only needed for URL conversion, YouTube, Azure Document Intelligence |
exiftool |
Optional | For image/audio EXIF metadata extraction |
ffmpeg |
Optional | For audio format handling (included in Docker image) |
External Service Dependencies
| Service | Required For | Auth Method |
|---|---|---|
| Azure Document Intelligence | -d / docintel_endpoint option |
Azure DefaultCredential or explicit credential |
| OpenAI-compatible API | LLM image descriptions, OCR plugin | API key via client object |
| YouTube | Transcript extraction | No auth (public API) |
3. Configuration Reference
MarkItDown has no configuration files. All settings are passed at runtime through constructor parameters or CLI flags.
MarkItDown Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enable_builtins |
bool |
True |
Register built-in converters. Set to False to start with an empty converter chain. |
enable_plugins |
bool |
None (disabled) |
Load and register third-party plugin converters |
llm_client |
Any | None |
OpenAI-compatible client for image descriptions and OCR |
llm_model |
str |
None |
Model name for LLM operations (e.g., "gpt-4o") |
llm_prompt |
str |
"Write a detailed caption for this image." |
Custom prompt for LLM image descriptions |
exiftool_path |
str |
Auto-detected | Full path to the exiftool binary |
style_map |
str |
None |
Custom style mapping for mammoth DOCX conversion |
requests_session |
requests.Session |
New session | Custom requests session for HTTP operations |
docintel_endpoint |
str |
None |
Azure Document Intelligence endpoint URL |
docintel_credential |
Any | None |
Azure credential object for Document Intelligence |
docintel_file_types |
list | None |
File types to process with Document Intelligence |
docintel_api_version |
str |
None |
Document Intelligence API version override |
Important
When
enable_plugins=Trueis set, all installed plugins are loaded and their converters registered. This executes third-party code. Only enable plugins in trusted environments and audit installed plugin packages before enabling.
Environment Variables
| Variable | Used By | Purpose |
|---|---|---|
EXIFTOOL_PATH |
Core library | Override exiftool binary location |
MARKITDOWN_ENABLE_PLUGINS |
MCP server | Enable plugins in MCP context (true/1/yes) |
ExifTool Auto-Detection
When exiftool_path is not explicitly set and EXIFTOOL_PATH is not in the environment, MarkItDown searches for the binary using shutil.which("exiftool") and validates it is in a trusted directory:
| Platform | Trusted Directories |
|---|---|
| Linux/macOS | /usr/bin, /usr/local/bin, /opt, /opt/bin, /opt/local/bin, /opt/homebrew/bin |
| Windows | C:\Windows\System32, C:\Program Files, C:\Program Files (x86) |
If the binary is found outside these directories, it is ignored for security. Set EXIFTOOL_PATH or exiftool_path explicitly in that case.
Docker Environment
The official Dockerfile sets:
| Variable | Value |
|---|---|
EXIFTOOL_PATH |
/usr/bin/exiftool |
FFMPEG_PATH |
/usr/bin/ffmpeg |
Build arguments:
| Arg | Default | Purpose |
|---|---|---|
INSTALL_GIT |
false |
Install git in the container |
USERID |
nobody |
User to run the container as |
GROUPID |
nogroup |
Group to run the container as |
4. Deployment & Scaling
Standalone CLI Deployment
Install via pip in a virtual environment. No server process, no configuration files, no persistent state.
python -m venv /opt/markitdown
source /opt/markitdown/bin/activate
pip install 'markitdown[all]'
Docker Deployment
docker build -t markitdown:latest .
The Docker image is based on python:3.13-slim-bullseye and includes:
- All Python dependencies via
markitdown[all] ffmpegfor audio processingexiftoolfor image/audio metadata- The sample plugin (RTF support)
Run as a one-shot converter:
docker run --rm -i markitdown:latest < input.pdf > output.md
MCP Server Deployment
STDIO Mode
For direct integration with LLM tools that support STDIO transport:
markitdown-mcp
HTTP/SSE Mode
For network-accessible deployment (localhost only by default):
markitdown-mcp --http --host 127.0.0.1 --port 3001
Endpoints exposed:
| Path | Transport | Purpose |
|---|---|---|
/mcp |
Streamable HTTP | Primary MCP endpoint |
/sse |
Server-Sent Events | SSE connection endpoint |
/messages/ |
POST | SSE message endpoint |
MCP Docker Deployment
docker build -t markitdown-mcp:latest packages/markitdown-mcp/
docker run -it --rm markitdown-mcp:latest
For file access, mount volumes:
docker run -it --rm -v /data:/workdir markitdown-mcp:latest
Scaling Considerations
MarkItDown is a stateless, synchronous library. For high-throughput scenarios:
- Horizontal scaling: Run multiple worker processes, each with its own
MarkItDowninstance - Memory management: The PDF converter processes pages individually and calls
page.close()after each to limit memory usage - LLM rate limits: When using
llm_clientfor image descriptions or OCR, be aware of API rate limits on the LLM provider - Non-seekable streams: If the input stream is not seekable, MarkItDown reads the entire content into an
io.BytesIObuffer in memory
Note
Each
MarkItDowninstance is lightweight and stateless — creating one per request is safe and recommended in multi-threaded environments to avoid converter registration conflicts.
5. Operations
Health Monitoring
MarkItDown has no built-in health check endpoint. For the MCP server, monitor the HTTP endpoints:
curl -s http://127.0.0.1:3001/mcp
Tip
For production MCP deployments, wrap the server behind a reverse proxy that provides health checks, TLS termination, and authentication — none of which the MCP server provides natively.
Logging
MarkItDown uses Python's print() for error output in a few places (YouTube transcript errors, MCP server lifecycle messages). It does not use the logging module. For production use, redirect stderr:
markitdown-mcp --http 2>>/var/log/markitdown-mcp.log
Note
Plugin loading failures and converter registration failures emit
UserWarningvia Python'swarningsmodule, notprint(). Capture these with-W allor by settingwarnings.simplefilter("always")in your wrapper code.
Testing
MarkItDown uses hatch as its test runner:
cd packages/markitdown
pip install hatch
hatch test
Test suites:
| Test File | Covers |
|---|---|
test_cli_misc.py |
CLI edge cases and option handling |
test_cli_vectors.py |
CLI conversion of test vector files |
test_module_misc.py |
Python API edge cases |
test_module_vectors.py |
Python API conversion of test vector files |
test_docintel_html.py |
Azure Document Intelligence HTML output |
test_pdf_masterformat.py |
MasterFormat-style PDF numbering |
test_pdf_memory.py |
PDF memory usage regression tests |
test_pdf_tables.py |
PDF table extraction accuracy |
Pre-commit checks:
pre-commit run --all-files
6. Performance
PDF Processing
The PDF converter uses a two-pass strategy:
- pdfplumber pass: Every page is checked for form-style/tabular content using word position analysis. Pages with detected tables get rich Markdown table extraction. Plain text pages collect basic text.
- pdfminer fallback: If no pages had form-style content, the entire document is re-processed through
pdfminer.high_level.extract_text()for better text spacing in prose documents.
Memory optimization: page.close() is called after each page to free pdfplumber's cached objects, keeping memory usage constant regardless of page count.
Table Detection Algorithm
The form content extractor (_extract_form_content_from_words) uses adaptive column clustering:
- Groups words by Y position (rows) with 5px tolerance
- Identifies column boundaries from X positions of multi-column rows
- Computes adaptive clustering tolerance using 70th percentile gap analysis (clamped to 25-50px range)
- Classifies rows as table vs. paragraph based on column alignment
- Requires at least 20% of rows to be table rows before applying table formatting
- Filters out MasterFormat-style partial numbering (e.g.,
.1,.2) from table detection
Converter Priority System
Converters are sorted by priority before each conversion attempt. Lower values are tried first:
| Priority | Converters | Purpose |
|---|---|---|
| -1.0 | OCR plugin converters | Override built-in converters when OCR is available |
| 0.0 | All specific format converters (PDF, DOCX, XLSX, etc.) | Match specific file types |
| 10.0 | PlainText, HTML, ZIP | Catch-all generic converters |
Within the same priority level, most recently registered converters are tried first (stable sort, LIFO insertion).
File Type Detection Pipeline
For each conversion, MarkItDown generates a list of StreamInfo guesses:
- Extension/MIME inference: If extension is known but MIME type is not (or vice versa), infer the missing one using Python's
mimetypesmodule - Magika content detection: Google's Magika library analyzes the stream content to identify the file type
- Charset detection: For text files,
charset-normalizeranalyzes the first 4KB to guess the encoding - Compatibility check: If the Magika guess is compatible with the base guess (matching MIME type, extension, charset), they are merged into one guess. If incompatible, both guesses are tried.
- Fallback: An empty
StreamInfo()is always appended as the final fallback
7. Security
Input Validation
- MIME type validation: The CLI validates that MIME type hints contain exactly one
/ - Charset validation: Character set hints are validated via
codecs.lookup()before use - XML handling: Uses
defusedxmlfor safe XML parsing (prevents XXE attacks) - File URI handling:
file:URIs must have an empty netloc orlocalhost; non-local file URIs are rejected
Warning
MarkItDown processes arbitrary file content, including content fetched from user-provided URLs. In multi-tenant or untrusted-input scenarios, run conversions in a sandboxed environment (container, restricted user) to limit blast radius from malformed input.
ExifTool Security
The exiftool auto-detection only trusts binaries found in a hardcoded list of well-known system directories. This prevents path injection attacks where a malicious exiftool binary in a user-controlled directory could execute arbitrary code.
MCP Server Security
Caution
The MCP server has NO authentication. It runs with the full privileges of the user who started it. The
convert_to_markdowntool can read any file accessible to that user and fetch any network resource.
- Binds to
127.0.0.1by default in HTTP mode - Prints a prominent security warning to stderr if bound to a non-localhost interface
- The
MARKITDOWN_ENABLE_PLUGINSenvironment variable must be explicitly set to enable third-party plugins in MCP mode
Data URI Processing
Data URIs are parsed manually (not via standard library) with explicit handling for base64 vs. percent-encoded content. Malformed data URIs (missing comma separator) raise ValueError.
HTTP Request Headers
When fetching URLs, MarkItDown sends a custom Accept header that prefers Markdown over HTML. This can reveal that a MarkItDown client is making the request, which may be relevant in security-sensitive contexts.
8. Developer Internals
Repository Structure
microsoft/markitdown/
├── packages/
│ ├── markitdown/ # Core library
│ │ ├── pyproject.toml # Package config, dependencies, extras
│ │ ├── src/markitdown/
│ │ │ ├── __about__.py # Version: 0.1.6b2
│ │ │ ├── __init__.py # Public API exports
│ │ │ ├── __main__.py # CLI entry point (argparse)
│ │ │ ├── _markitdown.py # Core MarkItDown class, converter chain
│ │ │ ├── _base_converter.py # DocumentConverter and DocumentConverterResult
│ │ │ ├── _stream_info.py # StreamInfo dataclass
│ │ │ ├── _uri_utils.py # file: and data: URI parsing
│ │ │ ├── _exceptions.py # Exception hierarchy
│ │ │ ├── converters/ # Built-in format converters
│ │ │ │ ├── _pdf_converter.py
│ │ │ │ ├── _docx_converter.py
│ │ │ │ ├── _xlsx_converter.py
│ │ │ │ ├── _pptx_converter.py
│ │ │ │ ├── _html_converter.py
│ │ │ │ ├── _image_converter.py
│ │ │ │ ├── _audio_converter.py
│ │ │ │ ├── _csv_converter.py
│ │ │ │ ├── _epub_converter.py
│ │ │ │ ├── _youtube_converter.py
│ │ │ │ ├── _wikipedia_converter.py
│ │ │ │ ├── _rss_converter.py
│ │ │ │ ├── _bing_serp_converter.py
│ │ │ │ ├── _ipynb_converter.py
│ │ │ │ ├── _outlook_msg_converter.py
│ │ │ │ ├── _zip_converter.py
│ │ │ │ ├── _plain_text_converter.py
│ │ │ │ ├── _doc_intel_converter.py
│ │ │ │ ├── _llm_caption.py
│ │ │ │ ├── _markdownify.py
│ │ │ │ └── _exiftool.py
│ │ │ └── converter_utils/
│ │ │ └── docx/
│ │ │ ├── pre_process.py # DOCX preprocessing
│ │ │ └── math/
│ │ │ ├── omml.py # Office MathML parser
│ │ │ └── latex_dict.py # OMML-to-LaTeX mappings
│ │ └── tests/ # Test suite
│ ├── markitdown-mcp/ # MCP server package
│ │ ├── pyproject.toml
│ │ ├── Dockerfile
│ │ └── src/markitdown_mcp/
│ │ └── __main__.py # MCP server entry point
│ ├── markitdown-ocr/ # OCR plugin package
│ │ ├── pyproject.toml
│ │ └── src/markitdown_ocr/
│ │ ├── _plugin.py # Plugin registration
│ │ ├── _ocr_service.py # LLM Vision OCR service
│ │ ├── _pdf_converter_with_ocr.py
│ │ ├── _docx_converter_with_ocr.py
│ │ ├── _pptx_converter_with_ocr.py
│ │ └── _xlsx_converter_with_ocr.py
│ └── markitdown-sample-plugin/ # Reference plugin (RTF)
│ └── src/markitdown_sample_plugin/
│ └── _plugin.py # RTF converter + registration
├── Dockerfile # Main Docker image
├── README.md
├── LICENSE # MIT
└── .pre-commit-config.yaml
Core Class Architecture
MarkItDown (_markitdown.py)
The central orchestrator. Responsibilities:
- Converter registry: Maintains an ordered list of
ConverterRegistrationobjects, each wrapping aDocumentConverterand its priority - Input normalization:
convert()accepts multiple input types and routes toconvert_local(),convert_stream(),convert_uri(), orconvert_response() - Type detection:
_get_stream_info_guesses()combines extension, MIME type, Magika detection, and charset detection into a list ofStreamInfoguesses - Conversion loop:
_convert()iterates through all (guess, converter) pairs in priority order, callingaccepts()thenconvert(). First successful result wins. - Output normalization: Strips trailing whitespace from lines, collapses 3+ consecutive newlines to 2
Key invariant: The file stream position must not change between converter attempts. _convert() asserts this after every accepts() call and resets position after every convert() call.
DocumentConverter (_base_converter.py)
Abstract base class for all converters:
class DocumentConverter:
def accepts(self, file_stream: BinaryIO, stream_info: StreamInfo, **kwargs) -> bool:
"""Quick check if this converter should attempt conversion."""
raise NotImplementedError
def convert(self, file_stream: BinaryIO, stream_info: StreamInfo, **kwargs) -> DocumentConverterResult:
"""Perform the actual conversion."""
raise NotImplementedError
The **kwargs propagate global settings from the MarkItDown instance: llm_client, llm_model, llm_prompt, style_map, exiftool_path, and _parent_converters (for recursive conversion in ZIP files).
DocumentConverterResult (_base_converter.py)
@dataclass
class DocumentConverterResult:
markdown: str # The converted Markdown text
title: str | None # Optional document title
The text_content property is a soft-deprecated alias for markdown.
StreamInfo (_stream_info.py)
Frozen dataclass carrying file metadata through the conversion pipeline:
@dataclass(kw_only=True, frozen=True)
class StreamInfo:
mimetype: str | None # e.g., "application/pdf"
extension: str | None # e.g., ".pdf" (with dot)
charset: str | None # e.g., "utf-8"
filename: str | None # Original filename
local_path: str | None # If read from disk
url: str | None # If read from URL
copy_and_update() creates a new instance with updated fields (non-None values from the argument override existing values).
Exception Hierarchy
MarkItDownException
├── MissingDependencyException # Optional dep not installed
├── UnsupportedFormatException # No converter matched
└── FileConversionException # Converter(s) tried but failed
└── .attempts: list[FailedConversionAttempt]
└── .converter, .exc_info
MissingDependencyException includes a formatted error message directing users to install the correct optional dependency extra.
Converter Implementation Patterns
Every converter follows the same pattern:
-
Lazy dependency loading: Optional dependencies are imported at module level inside a try/except. If import fails,
_dependency_exc_infois stored and re-raised inconvert()asMissingDependencyException. -
Acceptance check:
accepts()checksstream_info.extensionandstream_info.mimetypeagainst hardcoded lists. URL-based converters (YouTube, Wikipedia) also checkstream_info.url. -
Conversion:
convert()reads fromfile_stream, processes the content, and returnsDocumentConverterResult.
Example pattern:
_dependency_exc_info = None
try:
import some_library
except ImportError:
_dependency_exc_info = sys.exc_info()
ACCEPTED_MIME_TYPE_PREFIXES = ["application/pdf"]
ACCEPTED_FILE_EXTENSIONS = [".pdf"]
class MyConverter(DocumentConverter):
def accepts(self, file_stream, stream_info, **kwargs):
mimetype = (stream_info.mimetype or "").lower()
extension = (stream_info.extension or "").lower()
return extension in ACCEPTED_FILE_EXTENSIONS or \
any(mimetype.startswith(p) for p in ACCEPTED_MIME_TYPE_PREFIXES)
def convert(self, file_stream, stream_info, **kwargs):
if _dependency_exc_info is not None:
raise MissingDependencyException(...)
# ... actual conversion logic
return DocumentConverterResult(markdown=result)
Plugin System
Plugins use Python's importlib.metadata.entry_points with the group "markitdown.plugin".
Plugin interface contract:
- The plugin module must have
__plugin_interface_version__ = 1 - The module must export a
register_converters(markitdown: MarkItDown, **kwargs)function - This function receives the
MarkItDowninstance and all constructor kwargs - It calls
markitdown.register_converter(converter, priority=...)for each converter
Registration in pyproject.toml:
[project.entry-points."markitdown.plugin"]
my_plugin = "my_package._plugin"
Plugin loading:
- Plugins are lazy-loaded once per process via
_load_plugins()(global singleton) - Failed plugin loads emit a
UserWarningbut do not crash the application - Failed converter registrations also emit warnings without crashing
- Plugins are disabled by default; pass
enable_plugins=Trueto the constructor
Priority system for plugins:
- Built-in specific converters: priority
0.0(PRIORITY_SPECIFIC_FILE_FORMAT) - Built-in generic converters: priority
10.0(PRIORITY_GENERIC_FILE_FORMAT) - OCR plugin converters: priority
-1.0(runs before built-ins, effectively replacing them) - Plugins can use any float value for priority
OCR Plugin Architecture
The markitdown-ocr plugin provides OCR-enhanced versions of four built-in converters:
| OCR Converter | Replaces | Priority |
|---|---|---|
PdfConverterWithOCR |
PdfConverter |
-1.0 |
DocxConverterWithOCR |
DocxConverter |
-1.0 |
PptxConverterWithOCR |
PptxConverter |
-1.0 |
XlsxConverterWithOCR |
XlsxConverter |
-1.0 |
The OCR service (LLMVisionOCRService) uses the same llm_client/llm_model pattern as the built-in image description feature. If no LLM client is provided, the OCR converters still load but silently skip OCR, falling back to standard text extraction.
MCP Server Architecture
The markitdown-mcp server uses FastMCP from the mcp library and exposes a single tool:
@mcp.tool()
async def convert_to_markdown(uri: str) -> str:
"""Convert a resource described by an http:, https:, file: or data: URI to markdown"""
return MarkItDown(enable_plugins=check_plugins_enabled()).convert_uri(uri).markdown
Transport options:
| Transport | Implementation | When |
|---|---|---|
| STDIO | mcp.run() via FastMCP |
Default (no flags) |
| Streamable HTTP | StreamableHTTPSessionManager via Starlette |
--http flag |
| SSE | SseServerTransport via Starlette |
--http flag (same server) |
The HTTP server creates a Starlette application with three routes:
/sse— SSE connection endpoint/mcp— Streamable HTTP endpoint/messages/— SSE message POST endpoint
The session manager is configured as stateless (stateless=True) with JSON responses.
Note
A new
MarkItDowninstance is created for every MCP tool invocation. This is intentional — the instance is lightweight, and creating per-request avoids state leakage between calls.
Code Patterns and Conventions
Naming:
- Private modules prefixed with
_(e.g.,_markitdown.py,_pdf_converter.py) - Public API exported through
__init__.pywith__all__ - Converter classes named
{Format}Converter(e.g.,PdfConverter,DocxConverter)
Type annotations:
- Full type annotations throughout, including
Optional,Union,BinaryIO py.typedmarker files for PEP 561 type checking support in all packagesStreamInfouseskw_only=True, frozen=Truedataclass for immutability
Error handling:
- Converters swallow exceptions during
_convert()and track them asFailedConversionAttempt - Only raises
FileConversionExceptionif all converters that accepted the file failed - Plugin loading failures are non-fatal (warning + skip)
Stream safety:
- Every converter must leave
file_streamat its original position (asserted in_convert()) - Non-seekable streams are automatically buffered into
io.BytesIO file_stream.seek(cur_pos)infinallyblocks ensures position reset even on exceptions
Deprecation pattern:
text_contentproperty onDocumentConverterResult(usemarkdowninstead)convert_url()method (useconvert_uri()instead)register_page_converter()method (useregister_converter()instead)file_extensionandurlkwargs on conversion methods (usestream_infoinstead)
Testing Strategy
Tests are organized into two categories:
-
Vector tests (
test_cli_vectors.py,test_module_vectors.py): Convert known test files and assert expected outputs. Test files are stored intests/test_files/with expected outputs intests/test_files/expected_outputs/. -
Miscellaneous tests (
test_cli_misc.py,test_module_misc.py): Edge cases, error handling, option parsing. -
Specialized tests: PDF-specific tests for MasterFormat numbering, memory usage, and table extraction.
Test infrastructure:
_test_vectors.pycontains shared test vector definitions- Test environment uses
hatch testwith the[all]feature group plusopenaifor mock LLM tests
Potential Improvements
Based on code analysis and community issues:
- Structured logging: Replace
print()calls with Pythonloggingmodule for production-grade observability - Async support: The MCP server is async but the core library is synchronous. An async conversion API would avoid blocking the event loop for large files.
- Short URL support: GitHub issue #1775 reports YouTube short URLs and Shorts are not recognized by the YouTube converter
- Streaming output: For very large documents, a streaming/chunked output mode could reduce memory usage
Contributing
MarkItDown uses the Microsoft CLA (Contributor License Agreement). Key development workflows:
- Fork and clone the repository
- Install development dependencies:
pip install -e 'packages/markitdown[all]' - Run tests:
cd packages/markitdown && hatch test - Run pre-commit checks:
pre-commit run --all-files - Run type checking:
hatch run types:check
For plugin contributions, use packages/markitdown-sample-plugin as a template. Create a new package with a register_converters() function and register it via the markitdown.plugin entry point group.
9. Integration Points
Python Library Integration
from markitdown import (
MarkItDown,
DocumentConverter,
DocumentConverterResult,
StreamInfo,
PRIORITY_SPECIFIC_FILE_FORMAT,
PRIORITY_GENERIC_FILE_FORMAT,
)
Custom Converter Registration
Register converters at runtime:
md = MarkItDown(enable_builtins=False) # Start empty
md.register_converter(MyConverter(), priority=0.0)
md.enable_builtins() # Add built-ins after custom converters
Or with built-ins enabled (custom converter gets higher priority within the same priority level due to LIFO insertion):
md = MarkItDown()
md.register_converter(MyConverter(), priority=0.0)
MCP Client Integration
Connect to the MCP server from any MCP-compatible client:
STDIO: Start markitdown-mcp as a subprocess, communicate via stdin/stdout
Streamable HTTP: POST to http://127.0.0.1:3001/mcp
SSE: Connect to http://127.0.0.1:3001/sse, post messages to /messages/
Debugging with MCP Inspector
npx @modelcontextprotocol/inspector
Connection options:
- STDIO: command =
markitdown-mcp - Streamable HTTP: URL =
http://127.0.0.1:3001/mcp - SSE: URL =
http://127.0.0.1:3001/sse
HTTP Accept Header
MarkItDown's default requests session sends:
Accept: text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.1
This enables Markdown-native responses from servers that support it (e.g., Cloudflare's markdown-for-agents feature).