Jesse A. Green / notebook

MarkItDown Administrator & Developer Guide

Generated May 2026 from microsoft/markitdown at 604bba1

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

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.0 always run before built-in converters at 0.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=True is 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]
  • ffmpeg for audio processing
  • exiftool for 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 MarkItDown instance
  • Memory management: The PDF converter processes pages individually and calls page.close() after each to limit memory usage
  • LLM rate limits: When using llm_client for 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.BytesIO buffer in memory

Note

Each MarkItDown instance 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 UserWarning via Python's warnings module, not print(). Capture these with -W all or by setting warnings.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:

  1. 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.
  2. 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:

  1. Groups words by Y position (rows) with 5px tolerance
  2. Identifies column boundaries from X positions of multi-column rows
  3. Computes adaptive clustering tolerance using 70th percentile gap analysis (clamped to 25-50px range)
  4. Classifies rows as table vs. paragraph based on column alignment
  5. Requires at least 20% of rows to be table rows before applying table formatting
  6. 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:

  1. Extension/MIME inference: If extension is known but MIME type is not (or vice versa), infer the missing one using Python's mimetypes module
  2. Magika content detection: Google's Magika library analyzes the stream content to identify the file type
  3. Charset detection: For text files, charset-normalizer analyzes the first 4KB to guess the encoding
  4. 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.
  5. 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 defusedxml for safe XML parsing (prevents XXE attacks)
  • File URI handling: file: URIs must have an empty netloc or localhost; 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_markdown tool can read any file accessible to that user and fetch any network resource.

  • Binds to 127.0.0.1 by default in HTTP mode
  • Prints a prominent security warning to stderr if bound to a non-localhost interface
  • The MARKITDOWN_ENABLE_PLUGINS environment 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:

  1. Converter registry: Maintains an ordered list of ConverterRegistration objects, each wrapping a DocumentConverter and its priority
  2. Input normalization: convert() accepts multiple input types and routes to convert_local(), convert_stream(), convert_uri(), or convert_response()
  3. Type detection: _get_stream_info_guesses() combines extension, MIME type, Magika detection, and charset detection into a list of StreamInfo guesses
  4. Conversion loop: _convert() iterates through all (guess, converter) pairs in priority order, calling accepts() then convert(). First successful result wins.
  5. 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:

  1. Lazy dependency loading: Optional dependencies are imported at module level inside a try/except. If import fails, _dependency_exc_info is stored and re-raised in convert() as MissingDependencyException.

  2. Acceptance check: accepts() checks stream_info.extension and stream_info.mimetype against hardcoded lists. URL-based converters (YouTube, Wikipedia) also check stream_info.url.

  3. Conversion: convert() reads from file_stream, processes the content, and returns DocumentConverterResult.

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:

  1. The plugin module must have __plugin_interface_version__ = 1
  2. The module must export a register_converters(markitdown: MarkItDown, **kwargs) function
  3. This function receives the MarkItDown instance and all constructor kwargs
  4. 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 UserWarning but do not crash the application
  • Failed converter registrations also emit warnings without crashing
  • Plugins are disabled by default; pass enable_plugins=True to 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 MarkItDown instance 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__.py with __all__
  • Converter classes named {Format}Converter (e.g., PdfConverter, DocxConverter)

Type annotations:

  • Full type annotations throughout, including Optional, Union, BinaryIO
  • py.typed marker files for PEP 561 type checking support in all packages
  • StreamInfo uses kw_only=True, frozen=True dataclass for immutability

Error handling:

  • Converters swallow exceptions during _convert() and track them as FailedConversionAttempt
  • Only raises FileConversionException if all converters that accepted the file failed
  • Plugin loading failures are non-fatal (warning + skip)

Stream safety:

  • Every converter must leave file_stream at its original position (asserted in _convert())
  • Non-seekable streams are automatically buffered into io.BytesIO
  • file_stream.seek(cur_pos) in finally blocks ensures position reset even on exceptions

Deprecation pattern:

  • text_content property on DocumentConverterResult (use markdown instead)
  • convert_url() method (use convert_uri() instead)
  • register_page_converter() method (use register_converter() instead)
  • file_extension and url kwargs on conversion methods (use stream_info instead)

Testing Strategy

Tests are organized into two categories:

  1. Vector tests (test_cli_vectors.py, test_module_vectors.py): Convert known test files and assert expected outputs. Test files are stored in tests/test_files/ with expected outputs in tests/test_files/expected_outputs/.

  2. Miscellaneous tests (test_cli_misc.py, test_module_misc.py): Edge cases, error handling, option parsing.

  3. Specialized tests: PDF-specific tests for MasterFormat numbering, memory usage, and table extraction.

Test infrastructure:

  • _test_vectors.py contains shared test vector definitions
  • Test environment uses hatch test with the [all] feature group plus openai for mock LLM tests

Potential Improvements

Based on code analysis and community issues:

  1. Structured logging: Replace print() calls with Python logging module for production-grade observability
  2. 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.
  3. Short URL support: GitHub issue #1775 reports YouTube short URLs and Shorts are not recognized by the YouTube converter
  4. 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:

  1. Fork and clone the repository
  2. Install development dependencies: pip install -e 'packages/markitdown[all]'
  3. Run tests: cd packages/markitdown && hatch test
  4. Run pre-commit checks: pre-commit run --all-files
  5. 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).