Skip to main content
Glama

pandoc-mcp

An MCP server that converts documents between formats with Pandoc: markdown, HTML, DOCX, PDF (XeLaTeX), ODT, EPUB, PPTX, LaTeX, reStructuredText, Typst, AsciiDoc, Jupyter notebooks and everything else your pandoc reads or writes.

It is built on mcp-service-kit and runs over stdio (local) or HTTP with a bearer token (Docker). It is a reimplementation of mcp-pandoc with a few design changes: see Differences from mcp-pandoc.

For how it is built (layers, conversion flow, storage, security model), see docs/ARCHITECTURE.md.

Tools

Tool

What it does

convert_document

Convert one input to output_format. Input is exactly one of contents (text), contents_base64 (a binary file), document (id of a stored document) or input_path (local stdio only). Options: input_format, filename, standalone, table_of_contents, metadata, resources (ids of uploaded images the input refers to by file name), and the named assets defaults, reference_doc, template, filters.

get_document

Fetch a stored document (the latest if no id) with its metadata and content.

list_assets

List the server's named filters, defaults files, reference docs and templates, plus the pandoc version.

create_upload_link

HTTP only. Return a short-lived, upload-only link for files the model can't pass inline, plus an upload_id.

check_upload

List the files uploaded through a link, as document ids for document or resources. With wait_s it waits for them.

Each result is stored on the server and returned with an id and a download URL:

  • text output (markdown, HTML, LaTeX, ...) also comes back inline;

  • binary output (DOCX, PDF, ODT, EPUB, PPTX) comes back as an embedded base64 resource, up to PANDOC_MCP_MAX_INLINE_BYTES;

  • inline: false returns only the id and URL, which keeps large files out of the model's context.

The id can be passed as document to chain conversions, e.g. DOCX → markdown, edit, markdown → DOCX, without resending the bytes.

Uploads and images

To get a file the model can't pass inline, such as the user's own files or files on an agent's shell, the model calls create_upload_link. The link:

  • works only for uploading, with no access to anything else;

  • expires after PANDOC_MCP_UPLOAD_LINK_TTL_S (10 minutes);

  • stops working once it has accepted max_files files.

The user opens it in a browser, or the agent runs the curl -F file=@... command it shows. The model then calls check_upload(upload_id) to get the document ids. The uploader never sees the ids, and nobody needs the server's token.

Images are embedded by passing their ids as resources and referring to them by file name:

{"contents": "# Report\n\n![Sales](chart.png)", "output_format": "docx", "resources": ["<id>"]}

Only those files are readable; any other image path stays blocked by the sandbox.

Related MCP server: Pandoc Bridge

Mermaid diagrams

Mermaid diagrams written as fenced code blocks are rendered automatically, wherever mermaid-cli (mmdc) is installed. The Docker image includes it, with Debian's Chromium. The format depends on the output:

Output

Diagram becomes

html, revealjs and other HTML slides

inline SVG

pdf, latex, beamer

vector PDF figure

docx, odt, pptx, epub, typst, ...

2x PNG, embedded in the file

markdown, gfm, commonmark, ipynb

unchanged code block (GitHub etc. render it)

``` {.mermaid caption="Request flow" width="80%"}
flowchart LR
  User --> API --> DB[(Database)]
```
  • caption turns the diagram into a numbered figure, and width sizes it.

  • A diagram with a syntax error stays as code, and mermaid's error appears in the result's warnings, so the model can fix it and convert again.

  • Pass render_mermaid: false to keep the code blocks.

  • Without mmdc, e.g. a local install without Node, diagrams stay as code, and list_assets reports mermaid as unavailable.

Running

Local (stdio)

You need pandoc, plus xelatex for PDF output (sudo apt install pandoc texlive-xetex texlive-latex-extra, or the pandoc release).

uv sync
uv run pandoc-mcp --mode stdio
# Claude Code
claude mcp add pandoc -- uv --directory /path/to/pandoc-mcp run pandoc-mcp

In stdio mode, input_path and output_path are accepted, so a conversion can read and write files next to you directly.

Docker (HTTP)

The image includes pandoc and TeX Live (XeLaTeX).

cp .env.example .env            # set PANDOC_MCP_TOKEN (openssl rand -hex 32)
docker compose up -d --build
claude mcp add --transport http pandoc http://localhost:8301/mcp \
  --header "Authorization: Bearer $PANDOC_MCP_TOKEN"

The server also provides these routes:

  • GET /documents/<id>/<filename>: download a stored document. The id is a random uuid4.

  • GET /upload/<token> / POST /upload/<token>: the upload form and endpoint behind a link from create_upload_link. The link is the credential; there is no other way to upload.

Assets

Filters, defaults files, reference documents and templates are installed by the administrator in PANDOC_MCP_ASSETS_DIR. Tools refer to them by name:

assets/
  defaults/        *.yaml   pandoc defaults files      e.g. academic-pdf.yaml, report-html.yaml
  filters/         *.lua    Lua filters (or executable JSON filters)   e.g. pagebreak.lua
  reference-docs/  *.docx|odt|pptx   styling for Office output         e.g. default.docx
  templates/                pandoc templates

If a text asset's first line is a comment (#, -- or %), that line becomes its description in list_assets.

  • A reference doc is a copy of pandoc's default (pandoc -o custom.docx --print-default-data-file reference.docx) with the styles edited in Word or LibreOffice.

  • JSON filters must already be executable. The server never runs chmod on them.

  • In Docker, ./assets is mounted read-only, so you can add assets without rebuilding the image.

Configuration

Variable

Default

MCP_TRANSPORT, MCP_HOST, MCP_PORT, MCP_AUTH_ENABLED

stdio, 0.0.0.0, 3000 (8301 in Docker), true

from mcp-service-kit; CLI flags --mode --host --port --no-auth

PANDOC_MCP_TOKEN

Bearer token; required for HTTP unless --no-auth

PANDOC_MCP_PUBLIC_BASE_URL

http://localhost:8301

Base of download URLs

PANDOC_MCP_OUTPUT_DIR

data/documents

Document store

PANDOC_MCP_ASSETS_DIR

assets

Named assets

PANDOC_MCP_PANDOC_PATH

pandoc

Pandoc executable

PANDOC_MCP_PDF_ENGINE

xelatex

--pdf-engine for PDF output

PANDOC_MCP_TIMEOUT_S

120

Pandoc is killed after this

PANDOC_MCP_MAX_INPUT_BYTES

20 MB

Inline, base64-decoded or uploaded input

PANDOC_MCP_MAX_INLINE_CHARS

50000

Longest text returned inline

PANDOC_MCP_MAX_INLINE_BYTES

5 MB

Largest binary returned as embedded base64

PANDOC_MCP_RETENTION_DAYS

7

Stored documents are purged after this

PANDOC_MCP_MERMAID

true

Render mermaid code blocks when mmdc is available

PANDOC_MCP_MMDC_PATH

mmdc

mermaid-cli executable

PANDOC_MCP_MERMAID_THEME

default

default, neutral, dark or forest

PANDOC_MCP_PUPPETEER_CONFIG

(/etc/puppeteer.json in Docker)

Chromium settings for mmdc

PANDOC_MCP_UPLOAD_LINK_TTL_S

600

Lifetime of an upload link

PANDOC_MCP_UPLOAD_LINK_MAX_FILES

10

Most files one link may accept

PANDOC_MCP_UPLOAD_LINK_MAX_ACTIVE

100

Most links open at once

PANDOC_MCP_ALLOW_LOCAL_PATHS

stdio only

Accept input_path / output_path

Security model

These protections apply over HTTP:

  • No client-supplied paths.

    • input_path and output_path are disabled.

    • Assets are resolved by bare name inside the assets directory.

    • Upload filenames are reduced to a plain name.

  • The server token never leaves the MCP client. Uploads use links that are upload-only, short-lived and limited to a few files. Tokens are stored only as hashes, in memory, so a restart invalidates them. A link that ends up in a transcript stops working once it is used up or expires.

  • Pandoc runs with --sandbox. Readers and writers cannot read local files. For example, ![x](/etc/passwd) → DOCX would otherwise embed the file. Each conversion also runs in its own temporary directory. Images are embedded only when they are listed as resources.

  • TeX is confined. It runs with openin_any=p openout_any=p shell_escape=f, so raw LaTeX such as \input{/etc/passwd} is refused by xelatex.

  • Arguments go straight to pandoc. It is run as a subprocess with an argument list, never a shell, and is killed after PANDOC_MCP_TIMEOUT_S.

  • Defaults files can't change the target. --defaults is always placed first, so a defaults file cannot override the requested formats or the output file.

  • Mermaid renders in Chromium. Untrusted diagrams are rendered in headless Chromium, inside the container, with mermaid's default strict security level (no scripts or click handlers). Chromium's own sandbox is off, because it needs privileges the container does not have. Set PANDOC_MCP_MERMAID=false if that is not acceptable.

  • Filters are trusted code. They are whatever the administrator installed, so only install filters you trust.

Differences from mcp-pandoc

  • stdio protocol fixed. mcp-pandoc's print() calls wrote to stdout, which corrupts the stdio protocol. Logging here goes to stderr.

  • Non-blocking with a timeout. Pandoc runs as an async subprocess with a timeout instead of a blocking pypandoc call inside the event loop.

  • Formats come from pandoc itself. They are validated against pandoc --list-input-formats and --list-output-formats, not a fixed list of 10. That list offered PDF as an input and used txt where pandoc calls it plain.

  • No server paths needed. Binary formats come back inline as base64 or by URL. mcp-pandoc required a path on the server's filesystem.

  • Base64 input. Binary input can be sent as base64, so a model can convert files it holds or produced.

  • Named assets. Filters and defaults files are named, admin-installed assets instead of arbitrary paths, and filters are never chmod'ed.

  • Code structure. The code uses mcp-service-kit's layout (features/<tool>/tool.py, pydantic config, standard CLI), and tests run through a real MCP client.

Development

uv sync
uv run pytest     # tests needing pandoc / xelatex are skipped when those are missing
uv run ruff check

To run the whole suite, PDF included, inside the image:

docker build -t pandoc-mcp . && docker run --rm --user 0 -v $PWD/tests:/app/tests:ro \
  pandoc-mcp sh -c 'uv sync --frozen -q && uv run --frozen pytest -q -p no:cacheprovider'

Related MCP Connectors

Related MCP Servers