# MarkItDown Administrator & Developer Guide

## 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

```text
                            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.

```bash
python -m venv /opt/markitdown
source /opt/markitdown/bin/activate
pip install 'markitdown[all]'
```

### Docker Deployment

```bash
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:

```bash
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:

```bash
markitdown-mcp
```

#### HTTP/SSE Mode

For network-accessible deployment (localhost only by default):

```bash
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

```bash
docker build -t markitdown-mcp:latest packages/markitdown-mcp/
docker run -it --rm markitdown-mcp:latest
```

For file access, mount volumes:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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

```text
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:

```python
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`)

```python
@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:

```python
@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

```text
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:

```python
_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`:**

```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:

```python
@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

```python
from markitdown import (
    MarkItDown,
    DocumentConverter,
    DocumentConverterResult,
    StreamInfo,
    PRIORITY_SPECIFIC_FILE_FORMAT,
    PRIORITY_GENERIC_FILE_FORMAT,
)
```

### Custom Converter Registration

Register converters at runtime:

```python
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):

```python
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

```bash
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:

```text
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).

---

_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. Unofficial; not affiliated with or reviewed by the project._
