> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/microsoft/markitdown/llms.txt
> Use this file to discover all available pages before exploring further.

# Plugin Development

> Create and distribute MarkItDown plugins to share custom converters

Plugins allow you to package and distribute custom converters as standalone Python packages. MarkItDown automatically discovers and loads plugins using Python's entry points system.

## Plugin Architecture

MarkItDown uses the `markitdown.plugin` entry point group to discover plugins. When `enable_plugins=True`, MarkItDown calls each plugin's `register_converters()` function during initialization.

<Note>
  Plugins are **disabled by default**. Users must explicitly enable them with `enable_plugins=True` or the `--use-plugins` CLI flag.
</Note>

## Creating a Plugin

<Steps>
  ### Set Up the Package Structure

  Create a standard Python package structure:

  ```
  markitdown-sample-plugin/
  ├── src/
  │   └── markitdown_sample_plugin/
  │       ├── __init__.py
  │       ├── __about__.py
  │       └── _plugin.py
  ├── tests/
  │   ├── __init__.py
  │   ├── test_sample_plugin.py
  │   └── test_files/
  │       └── test.rtf
  ├── pyproject.toml
  └── README.md
  ```

  ### Implement the Plugin Interface

  In your main plugin module (e.g., `_plugin.py`), implement the required interface:

  ```python theme={null}
  from markitdown import (
      MarkItDown,
      DocumentConverter,
      DocumentConverterResult,
      StreamInfo,
  )
  from typing import BinaryIO, Any

  # REQUIRED: Plugin interface version
  __plugin_interface_version__ = 1

  # REQUIRED: Registration function
  def register_converters(markitdown: MarkItDown, **kwargs):
      """
      Called during MarkItDown construction to register converters.
      
      Parameters:
      - markitdown: The MarkItDown instance to register converters with
      - **kwargs: Additional configuration passed from MarkItDown constructor
      """
      markitdown.register_converter(RtfConverter())
  ```

  ### Implement Your Converter

  Create your custom converter class:

  ```python theme={null}
  class RtfConverter(DocumentConverter):
      """Converts RTF files to Markdown."""
      
      def accepts(
          self,
          file_stream: BinaryIO,
          stream_info: StreamInfo,
          **kwargs: Any,
      ) -> bool:
          mimetype = (stream_info.mimetype or "").lower()
          extension = (stream_info.extension or "").lower()
          
          if extension in [".rtf"]:
              return True
          
          if mimetype.startswith("text/rtf") or mimetype.startswith("application/rtf"):
              return True
          
          return False
      
      def convert(
          self,
          file_stream: BinaryIO,
          stream_info: StreamInfo,
          **kwargs: Any,
      ) -> DocumentConverterResult:
          from striprtf.striprtf import rtf_to_text
          import locale
          
          # Decode the file
          encoding = stream_info.charset or locale.getpreferredencoding()
          stream_data = file_stream.read().decode(encoding)
          
          # Convert to plain text
          markdown = rtf_to_text(stream_data)
          
          return DocumentConverterResult(
              markdown=markdown,
              title=None
          )
  ```

  ### Export the Interface

  In your `__init__.py`, export the required symbols:

  ```python theme={null}
  from ._plugin import (
      __plugin_interface_version__,
      register_converters,
      RtfConverter,
  )

  __all__ = [
      "__plugin_interface_version__",
      "register_converters",
      "RtfConverter",
  ]
  ```

  ### Configure pyproject.toml

  Add the entry point to your `pyproject.toml`:

  ```toml theme={null}
  [project]
  name = "markitdown-sample-plugin"
  dynamic = ["version"]
  description = "A sample plugin for the markitdown library."
  requires-python = ">=3.10"
  dependencies = [
      "markitdown>=0.1.0a1",
      "striprtf",
  ]

  # CRITICAL: This entry point enables plugin discovery
  [project.entry-points."markitdown.plugin"]
  sample_plugin = "markitdown_sample_plugin"
  ```

  The entry point format is:

  ```toml theme={null}
  [project.entry-points."markitdown.plugin"]
  <plugin_name> = "<package_name>"
  ```

  * `<plugin_name>`: A unique identifier for your plugin
  * `<package_name>`: The fully qualified name of your package
</Steps>

## Complete Example: RTF Plugin

Here's the complete RTF converter plugin from `packages/markitdown-sample-plugin/`:

```python theme={null}
import locale
from typing import BinaryIO, Any
from striprtf.striprtf import rtf_to_text

from markitdown import (
    MarkItDown,
    DocumentConverter,
    DocumentConverterResult,
    StreamInfo,
)

__plugin_interface_version__ = 1

ACCEPTED_MIME_TYPE_PREFIXES = [
    "text/rtf",
    "application/rtf",
]

ACCEPTED_FILE_EXTENSIONS = [".rtf"]

def register_converters(markitdown: MarkItDown, **kwargs):
    """
    Called during construction of MarkItDown instances.
    """
    markitdown.register_converter(RtfConverter())

class RtfConverter(DocumentConverter):
    """Converts RTF files to Markdown."""
    
    def accepts(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,
    ) -> bool:
        mimetype = (stream_info.mimetype or "").lower()
        extension = (stream_info.extension or "").lower()
        
        if extension in ACCEPTED_FILE_EXTENSIONS:
            return True
        
        for prefix in ACCEPTED_MIME_TYPE_PREFIXES:
            if mimetype.startswith(prefix):
                return True
        
        return False
    
    def convert(
        self,
        file_stream: BinaryIO,
        stream_info: StreamInfo,
        **kwargs: Any,
    ) -> DocumentConverterResult:
        # Read the file using the charset or system default
        encoding = stream_info.charset or locale.getpreferredencoding()
        stream_data = file_stream.read().decode(encoding)
        
        # Convert and return
        return DocumentConverterResult(
            title=None,
            markdown=rtf_to_text(stream_data),
        )
```

## Installing and Testing Your Plugin

<Steps>
  ### Install in Development Mode

  ```bash theme={null}
  cd markitdown-sample-plugin
  pip install -e .
  ```

  ### Verify Plugin Discovery

  Check that MarkItDown can find your plugin:

  ```bash theme={null}
  markitdown --list-plugins
  ```

  You should see your plugin listed in the output.

  ### Test with CLI

  Use the `--use-plugins` flag to enable plugins:

  ```bash theme={null}
  markitdown --use-plugins path-to-file.rtf
  ```

  ### Test with Python

  Enable plugins when creating the MarkItDown instance:

  ```python theme={null}
  from markitdown import MarkItDown

  md = MarkItDown(enable_plugins=True)
  result = md.convert("path-to-file.rtf")
  print(result.markdown)
  ```
</Steps>

## Writing Plugin Tests

Create tests to verify both direct converter usage and plugin loading:

```python theme={null}
import os
from markitdown import MarkItDown, StreamInfo
from markitdown_sample_plugin import RtfConverter

TEST_FILES_DIR = os.path.join(os.path.dirname(__file__), "test_files")

def test_converter():
    """Test the converter directly."""
    with open(os.path.join(TEST_FILES_DIR, "test.rtf"), "rb") as file_stream:
        converter = RtfConverter()
        result = converter.convert(
            file_stream=file_stream,
            stream_info=StreamInfo(
                mimetype="text/rtf",
                extension=".rtf",
                filename="test.rtf"
            ),
        )
        
        assert "Expected content" in result.markdown

def test_markitdown():
    """Test that MarkItDown loads the plugin correctly."""
    md = MarkItDown(enable_plugins=True)
    result = md.convert(os.path.join(TEST_FILES_DIR, "test.rtf"))
    
    assert "Expected content" in result.markdown
```

## Plugin Loading Process

MarkItDown loads plugins using this process (`_markitdown.py:65`):

1. When `enable_plugins=True`, MarkItDown calls `enable_plugins()` method
2. `_load_plugins()` discovers all `markitdown.plugin` entry points
3. Each entry point is loaded and its `register_converters()` function is called
4. If any plugin fails to load, a warning is issued and the plugin is skipped

```python theme={null}
def _load_plugins() -> List[Any]:
    """Lazy load plugins."""
    plugins = []
    for entry_point in entry_points(group="markitdown.plugin"):
        try:
            plugins.append(entry_point.load())
        except Exception:
            warn(f"Plugin '{entry_point.name}' failed to load")
    return plugins
```

## Best Practices

<Warning>
  **Version Compatibility**: Always set `__plugin_interface_version__ = 1` and specify minimum MarkItDown version in dependencies.
</Warning>

<Tip>
  **Graceful Degradation**: Handle import errors for optional dependencies and provide helpful error messages.
</Tip>

<Tip>
  **Priority Configuration**: Allow users to pass priority via `**kwargs` in `register_converters()`:

  ```python theme={null}
  def register_converters(markitdown: MarkItDown, **kwargs):
      priority = kwargs.get("my_converter_priority", 0.0)
      markitdown.register_converter(MyConverter(), priority=priority)
  ```
</Tip>

<Tip>
  **Documentation**: Include clear README with installation instructions and example usage.
</Tip>

## Publishing Your Plugin

To share your plugin with others:

1. **Naming Convention**: Use `markitdown-<name>-plugin` for package name
2. **PyPI Publishing**: Follow standard Python package publishing process
3. **Documentation**: Include clear installation and usage instructions
4. **Testing**: Ensure comprehensive test coverage

```bash theme={null}
# Build the package
python -m build

# Upload to PyPI
python -m twine upload dist/*
```

Users can then install your plugin:

```bash theme={null}
pip install markitdown-sample-plugin
```

## Next Steps

<CardGroup cols={2}>
  <Card title="Custom Converters" icon="code" href="/advanced/custom-converters">
    Learn more about implementing converters
  </Card>

  <Card title="Configuration" icon="gear" href="/advanced/configuration">
    Understand MarkItDown configuration options
  </Card>
</CardGroup>
