Back to Directory/Documentation

ezmd

Convert files, URLs and text to LLM-ready Markdown with paging, provenance and warnings.

DocumentationPythonv0.1.0

ezmd

Convert anything (documents, web pages, data files, code, archives, and more) to clean Markdown for AI tools. No silent loss. Apache-2.0.

ci license: Apache-2.0

ezmd turns an input into an intermediate representation and renders it as Markdown with YAML frontmatter under one of four output profiles, plus a JSON sidecar with block provenance and an explicit list of warnings. When something cannot be converted, ezmd says so instead of dropping it quietly.

Status: 0.1. Documents, web pages, data files, code, email, ebooks and archives convert today. OCR, audio and video come next; see the roadmap.

Install

From PyPI (Python 3.12 or newer):

pip install ezmd                           # or: uv tool install ezmd
pip install "ezmd[docs]"                   # optional: Docling for PDF layout (large, pulls torch)
uvx ezmd convert https://example.com
uvx ezmd-mcp                               # the MCP server

On Linux, install CPU torch first or ezmd[docs] pulls about 6 GB of CUDA wheels, and the Docling extra needs its models downloaded once; see docs/install.md.

From source (needs uv):

git clone https://github.com/larpey/ezmd && cd ezmd
uv sync --all-packages
uv sync --all-packages --extra docs          # optional: Docling for PDF layout (large, pulls torch)
uv sync --all-packages --extra data          # optional: pyarrow for Parquet

Check what is available on your machine (converters, extras, and system programs such as LibreOffice):

uv run ezmd capabilities
uv run ezmd doctor

The TypeScript client is on npm:

npm install @ezmd/sdk

Quickstart

CLI

uv run ezmd convert fixtures/office/docx-review/input.docx --profile compact
uv run ezmd convert fixtures/pdf/born-digital-report/input.pdf --profile rag
uv run ezmd convert fixtures/text/markdown-kitchen-sink/input.md --json
uv run ezmd detect fixtures/pdf/born-digital-report/input.pdf
uv run ezmd batch fixtures/text --recursive --out out/text

Warnings go to stderr as WARN [code] message. ezmd batch skips unchanged inputs when run again. URLs work too; they are fetched through an SSRF guard that refuses private and loopback addresses:

uv run ezmd convert https://example.com --profile compact

Every command and option is in the CLI reference.

Python library

import ezmd

result = ezmd.convert(
    "fixtures/office/docx-review/input.docx",
    profile="compact",
    on_progress=lambda p: print(p.stage, p.progress),
)
print(result.markdown)
for w in result.warnings:
    print(w.severity, w.kind, w.message)
print(len(result.chunks(chunk_tokens=200)), "rag chunks")  # other profiles render from the same conversion

result.render("rag") renders another profile without converting again. See docs/library.md.

MCP server

From a source checkout, point your MCP client at uv run ezmd-mcp. Claude Code:

claude mcp add ezmd -- uvx ezmd-mcp
claude mcp add ezmd -- uv --directory /path/to/ezmd run ezmd-mcp   # from a source checkout

Claude Desktop (claude_desktop_config.json), using the published package:

{
  "mcpServers": {
    "ezmd": {
      "command": "uvx",
      "args": ["ezmd-mcp", "--allowed-dirs", "/Users/you/Documents"]
    }
  }
}

From a source checkout, use "command": "uv" with "args": ["--directory", "/path/to/ezmd", "run", "ezmd-mcp", ...]. Other clients, tools, and the HTTP transport: docs/mcp.md.

REST API and web UI

Run the API in-process (inline queue, no Redis), then convert with curl:

uv run ezmd serve --port 8080
curl -s -F file=@fixtures/text/plain-utf8/input.txt -F profile=compact "http://127.0.0.1:8080/v1/convert?wait=30&format=md"

The web UI is served at / once it is built (needs Node 22 and pnpm); then open http://127.0.0.1:8080:

pnpm install && pnpm --filter @ezmd/web build

API reference: docs/api.md and docs/api/openapi.json.

Self-host with Docker

The Compose stack (Caddy, API, sandboxed workers, Redis) builds from this checkout:

bash deploy/bootstrap.sh --plain-http --build

Then open http://localhost:8080. Signed images for each release are on GHCR (ghcr.io/larpey/ezmd-api, ezmd-worker, ezmd-fetch-node). Everything else (TLS, API keys, backups, upgrades, every environment variable): docs/selfhost.md.

What it converts

FamilyFormats
DocumentsPDF (text layer by default; Docling layout with the docs extra), DOCX, PPTX, XLSX, ODF, RTF, legacy Office (.doc, .xls, .ppt) when LibreOffice is installed
Ebooks and notebooksEPUB, Jupyter notebooks
WebHTML pages and URLs (Trafilatura, rules-based, raw)
DataCSV, TSV, JSON, JSON Lines, YAML, TOML, XML, SQLite, Parquet (data extra); database connection strings are refused without echoing credentials
CodeSource files; repository archives (zip or tar) packed with a secret scan, with --converter code.repo_pack or from a GitHub repository URL
ArchivesZIP, TAR, and gzip/bzip2/xz TAR with bomb limits; 7z with the 7z extra
EmailEML, MBOX, Outlook MSG
SpecializedSEC EDGAR filings
TextPlain text, Markdown

The full list, with engines, extras, status, fixture counts, and thresholds, is the generated converter matrix. Local directories are not an input yet (zip the folder, or use ezmd batch to convert the files in it one by one). Audio, video, OCR, chat exports, and social platforms are on the roadmap.

Output profiles

  • full: fidelity first; full frontmatter, numbered headings, page markers, and a JSON sidecar.
  • compact: for pasting into a chat; minimal frontmatter, link list at the end, a 16,000-token budget.
  • rag: for indexing; chunk markers with breadcrumbs, and the chunks in the sidecar.
  • agent: tool output for agents; the body sits inside an untrusted-content fence.

Details: docs/output-format.md.

Why ezmd

  • No silent loss: every dropped or degraded element becomes a structured warning with a code and a suggestion.
  • Permissive by default: the default install pulls only allowlisted licenses; anything else is an opt-in extra.
  • One box: CLI, library, REST API, web UI, and MCP server share one conversion path, and the Compose stack runs on a single machine.
  • Output profiles: one conversion renders as full, compact, rag, or agent.
  • Hostile input is the default assumption: sandboxed workers, an SSRF guard, archive and size limits.

Contributing

See CONTRIBUTING.md (setup, the council process, the fixture process), SECURITY.md, and CODE_OF_CONDUCT.md.

License

Apache-2.0 (see LICENSE and NOTICE). Optional extras (for example 7z and nonfree) and some model weights carry their own licenses; they are listed in docs/licenses.md.

Installation

Source-derived launch command. Check the maintainer’s required arguments and credentials before running:

bash
uvx ezmd-mcp

Set up in your AI client

Merge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.

json
{
  "mcpServers": {
    "io-github-larpey-ezmd": {
      "command": "uvx",
      "args": [
        "ezmd-mcp"
      ]
    }
  }
}

Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the client’s tool list, then try a read-only example from its documentation.

Claude Desktop setup reference

Package

ezmd-mcppypi

Compatible MCP Clients

ezmd works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.

  • Claude Desktop~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.
  • Cursor~/.cursor/mcp.jsonRestart Cursor for changes to take effect.
  • VS Code.vscode/mcp.jsonReload VS Code window for changes to take effect.
  • Windsurf~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect.
  • Claude Code.mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.

Learn More