document-mcp
An MCP (Model Context Protocol) server exposing
documents.js's document-conversion,.odb, metadata, and font tooling — plusdocument-compute.js's units-typed formula evaluation — as MCP tools, so an MCP-speaking agent can convert, inspect, edit, and compute over docx/pptx/odt/odp/ods/odg/odf/pdf/odb/xlsx/markdown documents without writing TypeScript against either library directly.
document-mcp adds no conversion or editing logic of its own — every tool is a thin registerOperation() wrapper (src/register-operation.ts) around a document-operations DocumentOperation: that package holds the actual dispatch to documents.js's conversion functions, DocumentConverter port, and .odb/PDF readers, as one Zod schema plus one transport-agnostic run() per operation, shared with (eventually) a REST server and document-cli's own flag parsing rather than defined here a second time. document-cli is the sibling frontend over the identical underlying operations — a terminal CLI/TUI rather than an MCP server — so the two are independent consumers of one shared implementation and can expose different subsets of it. A convert_document call's fidelity — which (source, targetFormat) pairs round-trip losslessly, which are a best-effort reconstruction, and why — is exactly what documents.js's own Fidelity section documents, table included; it is not restated here.
graph TD
schema("document-schema.js")
ooxml("ooxml.js")
odf("odf.js")
pdfcodec("pdf-codec")
mdcodec("markdown-codec")
bytecodec("byte-codec")
documents("documents.js")
outline("document-outline.js")
compute("document-compute.js")
operations("document-operations")
mcp("document-mcp")
cli("document-cli")
schema --> outline
schema --> compute
schema --> mcp
schema --> ooxml
schema --> odf
schema --> pdfcodec
schema --> mdcodec
schema --> documents
ooxml --> documents
odf --> documents
pdfcodec --> documents
mdcodec --> documents
bytecodec --> pdfcodec
bytecodec --> documents
documents --> operations
outline --> operations
compute --> operations
operations --> mcp
documents --> cli
odf --> cli
pdfcodec --> cli
click schema "https://github.com/ExaDev/documents.js/tree/main/packages/document-schema.js" "document-schema.js"
click ooxml "https://github.com/ExaDev/documents.js/tree/main/packages/ooxml.js" "ooxml.js"
click odf "https://github.com/ExaDev/documents.js/tree/main/packages/odf.js" "odf.js"
click pdfcodec "https://github.com/ExaDev/documents.js/tree/main/packages/pdf-codec" "pdf-codec"
click mdcodec "https://github.com/ExaDev/documents.js/tree/main/packages/markdown-codec" "markdown-codec"
click bytecodec "https://github.com/ExaDev/documents.js/tree/main/packages/byte-codec" "byte-codec"
click documents "https://github.com/ExaDev/documents.js" "documents.js"
click outline "https://github.com/ExaDev/documents.js/tree/main/packages/document-outline.js" "document-outline.js"
click compute "https://github.com/ExaDev/documents.js/tree/main/packages/document-compute.js" "document-compute.js"
click operations "https://github.com/ExaDev/documents.js/tree/main/packages/document-operations" "document-operations"
click mcp "https://github.com/ExaDev/documents.js/tree/main/packages/document-mcp" "document-mcp"
click cli "https://github.com/ExaDev/documents.js/tree/main/packages/document-cli" "document-cli"
style mcp fill:#f9a825,stroke:#333,stroke-width:3px
Getting started
Run the server directly — no install step needed:
npx document-mcp
The server uses stdio transport (runs as a local process). This is supported by Claude Code, Claude Desktop, Codex CLI, Codex Desktop, and OpenCode directly. Claude Web (claude.ai), Claude Mobile, and ChatGPT require a remote HTTP MCP server — see Remote transport below.
Compatibility
| Client | Transport | Direct support |
|---|---|---|
| Claude Code (CLI) | stdio | ✅ |
| Claude Code (plugin) | stdio | ✅ |
| Claude Desktop | stdio | ✅ |
| Codex CLI | stdio | ✅ |
| Codex Desktop | stdio | ✅ |
| OpenCode | stdio | ✅ |
| Claude Team/Enterprise (org) | stdio (per-machine) | ✅ via managed settings |
| Claude Web (claude.ai) | HTTP/SSE only | ❌ needs remote transport |
| Claude Mobile (iOS/Android) | HTTP/SSE only | ❌ needs remote transport |
| ChatGPT (web/desktop) | HTTP only | ❌ needs remote transport |
Connecting from Claude Code
One-liner (adds the MCP server directly):
claude mcp add --transport stdio document-mcp — npx -y document-mcp
Or install as a Claude Code plugin (this repo is a plugin marketplace — includes auto-update on new releases):
From the terminal:
claude plugin marketplace add ExaDev/document-mcp
claude plugin install document-mcp@exadev
Or from within a running Claude Code session:
/plugin marketplace add ExaDev/document-mcp
/plugin install document-mcp@exadev
Run /reload-plugins to activate in an already-running session. In Claude Desktop or on claude.ai: Customize → Plugins → Browse plugins, search for document-mcp, and install.
Connecting from Codex CLI
codex mcp add document-mcp — npx -y document-mcp
Or via the Codex Desktop app: Settings → MCP Servers → + Add.
Or add to ~/.codex/config.toml manually:
[mcp_servers.document-mcp]
command = "npx"
args = ["-y", "document-mcp"]
Connecting from OpenCode
Add to opencode.json:
{
"mcp": {
"document-mcp": {
"type": "local",
"command": ["npx", "-y", "document-mcp"]
}
}
}
Connecting from Claude Desktop
Add to the mcpServers block in Claude Desktop's config file (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"document-mcp": {
"command": "npx",
"args": ["-y", "document-mcp"]
}
}
}
Or, for local development against a checkout of this repository rather than the published package, point command at the built binary directly:
{
"mcpServers": {
"document-mcp": {
"command": "node",
"args": ["/absolute/path/to/document-mcp/dist/bin.js"]
}
}
}
Connecting from Claude Team/Enterprise (organization)
Organization admins can deploy MCP server configurations centrally via server-managed settings (Admin Settings → Claude Code → Managed settings in the claude.ai console). A managed-settings.json entry for document-mcp enforces the connection across all Claude Code users in the org — no per-user setup needed. Admins can also allow/block specific MCP servers via allowedMcpServers/blockedMcpServers in the same file.
Remote transport (HTTP)
Claude Web, Claude Mobile, and ChatGPT only accept remote (HTTP/SSE) MCP servers — a local stdio process is not reachable from a browser or phone. To use document-mcp on those platforms, run it behind an HTTP transport:
npx document-mcp --transport http --port 3000
This starts a real Streamable HTTP listener bound to 127.0.0.1 (loopback only) on the given --port (default 3000), serving the identical tool set the stdio transport registers — one MCP server instance per request, built from the same createServer() factory. --port 0 asks the OS for a free port; the server reports the port it actually bound on stderr before serving requests. It answers on /mcp and 404s everywhere else; a request from an older HTTP+SSE-era client is served transparently alongside current clients on that same endpoint, so there is no separate --transport sse to choose.
Then add the server URL (e.g., https://your-host:3000/mcp) as a connector in Claude Web (claude.ai/customize/connectors) or ChatGPT (Settings → Connectors → Advanced → Enable Developer Mode → Create). Use a tunnel (Cloudflare Tunnel, ngrok) or deploy to a server with TLS — both platforms require HTTPS; loopback-only binding means the tunnel or reverse proxy must run on the same host as document-mcp itself.
Security note: this HTTP listener has no authentication and no Host/Origin allowlisting of its own — anyone who can reach it can call every tool, including ones that read and write arbitrary filesystem paths. It is safe by default only because it binds to loopback; whatever fronts it for remote access (tunnel, reverse proxy) is responsible for authenticating callers before traffic ever reaches this process.
Standalone binary
Every release also attaches a Node single-executable application build for Linux (x64 and arm64), Windows (x64 and arm64), and macOS (Apple Silicon and Intel) to that release's own GitHub Release assets — the entire server and its dependencies embedded in one file, needing no Node.js install or npx at all. It supports both stdio and --transport http exactly as above; point an MCP client's command at the downloaded binary directly instead of npx/node. Download the asset matching your platform from the package's tag on the Releases page and run it directly (chmod +x on Linux/macOS first).
Container image
Every release also publishes a multi-arch (linux/amd64 + linux/arm64) container image to GitHub Container Registry, wrapping the identical standalone --transport http binary above on a minimal distroless base rather than a Node install:
docker run -p 3000:3000 ghcr.io/exadev/document-mcp:VERSION --port 3000
The image always runs in --transport http mode — stdio mode is exec'd directly by an MCP client as a subprocess, which a container has no role in — and binds to 0.0.0.0 inside the container regardless of --port, so -p <host>:<container> reaches /mcp directly. Replace VERSION with the package's own exact release version; latest also tracks the newest release, matching document-rest's identical image. See Remote transport above for this listener's own lack of authentication.
Development
Requires Node.js >=20 and pnpm 11.6.0 (pinned via packageManager in package.json).
pnpm install
pnpm build # turbo -> tsdown -> dist/ (ESM + CJS + .d.ts)
pnpm typecheck # turbo -> tsc --noEmit
pnpm lint # turbo -> eslint . --fix --cache --max-warnings 0
pnpm test # turbo -> vitest run --project unit
pnpm test:workers # turbo -> vitest under the real Cloudflare Workers runtime (workerd) via @cloudflare/vitest-pool-workers, driving createServer() through an in-memory JSON-RPC pair
pnpm test:smoke # turbo -> tsdown then vitest --project smoke — spawns dist/bin.js as a real subprocess driven over genuine MCP stdio
Document I/O
Every tool that takes or produces document bytes goes through the same two hybrid shapes, documented once here rather than repeated per tool below.
Input (DocumentInput) is a union: either a filesystem path (the format is inferred from the file extension — docx, pptx, xlsx, odt, odp, ods, odg, odf, md/markdown, pdf), or inline bytesBase64 plus an explicit format (required, since inline bytes carry no filename to infer one from). Each ODF/OOXML template and macro-enabled variant also reads as its base format: .ott/.ots/.otp/.otg/.otf as odt/ods/odp/odg/odf, and .dotx/.potx/.xltx (templates) or .docm/.xlsm/.pptm (macro-enabled) as their OOXML base — a template is the same package with a -template mimetype, and a macro-enabled file carries a vbaProject part this library reads past without executing or re-emitting. .odb tools are the one exception: a .odb has no single DocumentFormat of its own (it is a database front end, not a document — tables, saved queries, and reports are three unrelated output shapes), so their source.path/source.bytesBase64 bytes are read directly with no format inference at all.
Output (DocumentOutput), on every tool that produces a document, is a single optional outputPath: supply it to have the tool write the result to that filesystem path (the response then reports { path, byteLength }); omit it to receive the bytes inline instead ({ bytesBase64, byteLength }, flagged large: true above 5 MB — advisory only, the bytes are never truncated or refused).
Tools
| Tool | Description |
|---|---|
convert_document | Converts a document from one supported format to another via documents.js's DocumentConverter port — docx, pptx, xlsx, odt, odp, ods, odg, odf, markdown, rtf, wpd, doc, xls, ppt, epub, csv, svg, and pdf. Not every (source, targetFormat) pair is direct; call list_document_conversions first. |
list_document_conversions | Lists every (source, target) format pair convert_document actually supports. |
metadata_read | Reads a document's title/author/subject/keywords/creator/producer/created-and-modified timestamps. Works across every supported format, including xlsx and odf. |
metadata_write | Patches a document's title/author/subject/keywords in place. Does not convert format — source and target format must match (or both be pdf); odf (a standalone formula document) is rejected as either, since it has no write path back out at all. |
document_create | Creates a fresh, blank document via documents.js's own live-view editors (createDocx/createOdt/createMarkdownEditor/...) — docx, pptx, odt, odp, ods, odg, pdf, or markdown. |
document_append_paragraphs | Appends one or more paragraphs — each optionally built from several independently-formatted runs — to the end of a docx, odt, or markdown document, through the same live-view editors document_create and document-cli's own TUI use. Does not convert format — source and targetFormat must match. docx and odt share an identical field set (underline, fontFamily, sizePt, colour, headingLevel, alignment); markdown supports a smaller, different set (hyperlink, inline code) and rejects a docx/odt-only field outright rather than silently dropping it. Covers wordprocessing paragraph/run editing only — slides, sheets, drawings, tables, lists, and images are a separate editing surface not exposed here yet. |
fonts | Lists every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length). |
describe_font_file | Reads a standalone .ttf/.otf font file and reports the family/bold/italic triple it declares about itself. |
docx_extras | Reads a docx's own comments, footnotes, header/footer parts, and numbering definitions — data the ContentDocument pivot cannot carry, so no other tool sees it. |
pdf_inspect | Parses a PDF and reports a summary (page count, per-page size and item-kind histogram, metadata, embedded image formats), or with full: true the entire parsed LayoutDocument as plain JSON — no $schema stamp, since that family moved to pdf-codec at document-schema.js 4.0.0 and lost its schema-stamped envelope. |
odm_to_pdf | Converts a .odm (ODF master document) to PDF. A .odm never carries its chapters' content inline, so each chapter resolves via a caller-supplied chapters href-to-document map and/or a chaptersDir searched by basename. |
from_package | Rebuilds real document bytes in a target format from a DocumentTree previously serialised to JSON (e.g. by a conversion tool's own onDocument/package-dump step). Only a package genuinely written by a current dump round-trips: the $schema URI it carries pins the document-schema.js release that wrote it, and a pre-4.0.0 dump (the flat { formatVersion, content, pages } envelope) is rejected with an error naming the pinned release, the flat-to-tree change, and the remedy — a layout-document dump gets its own pointer, naming the move to pdf-codec. |
outline_document | Projects a document's own table of contents as structured JSON: groups ({ text, level, children }) for headings, list items, slides, sheets, and draw pages, leaves ({ kind, text }) for the content between them. The outline is over the source's own native content — read directly via documents.js's readNativeDocumentTree (no bridging conversion, no discarded output bytes) and built by document-outline.js's buildOutline. |
compute_formula | Reads every formula a document embeds (docx/odt/markdown paragraphs and pptx/odp slide shapes; a table cell's own blocks too — docx's own reader recovers a real equation nested inside a table cell; structurally, the walk also covers a drawing page's own shape flow, though no writer in the family populates a formula there today; a spreadsheet's own cell-anchored formula objects; a standalone .odf formula document; and a formula nested inside another embedded object at any depth — e.g. a formula embedded in a drawing embedded in a spreadsheet) and evaluates each through document-compute.js's units-typed evaluate() — an agent's way to check whether a document's stated arithmetic actually checks out. A formula referencing a symbol needs that symbol's value supplied via bindings, keyed by the document's own symbol-table id; each formula reports its own outcome independently (evaluated, no-content, or error), so one formula needing more bindings never blocks the others. |
odb_tables | Lists every table an embedded .odb database declares — column names, types, and row data — across every storage tier documents.js supports (HSQLDB TEXT/CACHED/BINARY, Firebird gbak backups). |
odb_forms | Lists every form an .odb database declares, with each form's own data source and field-bound controls. |
odb_reports | Lists every report an .odb database declares, with each report's own data-source command, band/group structure, and rpt: formula expressions. |
odb_query | Runs a bounded SELECT (with optional JOINs of any kind, table aliases, a derived table in FROM, and IN/EXISTS subqueries) over an embedded .odb database's extracted tables — given directly as SQL or by naming a saved query. No database engine involved; no column aliases; an unsupported construct is reported as a tool error naming it, never silently ignored. |
odb_to_csv | Extracts exactly one named table from an embedded .odb database as CSV bytes. The table name is required whenever the database declares more than one table. |
odb_to_xlsx | Extracts every table an embedded .odb database declares into one xlsx workbook, one sheet per table. |
odb_render_report | Resolves one of an .odb database's own reports — its data-bound command run through the bounded SQL engine, its rpt: formulas evaluated, its bands laid out — and renders the result to docx, odt, or pdf. |
References
- documents.js — the library this server exposes.
- document-outline.js — the artefact-utilities package over document-schema.js's tree-form
DocumentTreewhosebuildOutlinepowersoutline_document. - document-compute.js — the units-typed
MathExpressionevaluator whoseevaluate()powerscompute_formula. - document-cli — the sibling CLI/TUI over the same library, whose toolchain this repository's scaffold mirrors.
- Model Context Protocol — the protocol this server implements, via
@modelcontextprotocol/server.
Gotchas
- Runtime dependencies are
document-operations+documents.js+@modelcontextprotocol/server+@modelcontextprotocol/node+zodonly;pdf-codec,odf.js, anddocument-schema.jsare devDependencies (test-support only).@modelcontextprotocol/nodeis the Node adapter (toNodeHandler) the--transport httplistener uses to bridge@modelcontextprotocol/server's web-standardcreateMcpHandleronto a plainnode:httpserver — see Remote transport above. Every tool's actual implementation — the conversion/editing/inspection/.odblogic previously defined directly insrc/tools/*.ts, including its own reach intodocument-outline.js'sbuildOutline/outlineLeafTextanddocument-compute.js'sevaluate()— now lives indocument-operationsas aDocumentOperation; this package's ownsrc/tools/*.tsare thinregisterOperation()wrappers (src/register-operation.ts) that register each one as an MCP tool and, for the two operations whose own errors need MCP-specific enrichment (odb_render_report'sOdbReportNotSpecifiedError,odm_to_pdf'sOdmUnresolvedSectionError), amapErrorhook. Every runtime reach intopdf-codec/odf.js—ProvidedFont/FontSubstitution/describeFontFace/theWinAnsisubstitution shape — goes throughdocuments.js's own re-exports (by way ofdocument-operations), so a published install pulls in no directpdf-codec/odf.jsdependency.odf.jsanddocument-schema.jssurvive indevDependenciessolely for this package's own test fixtures andcompute_formula's own test assertions (document-schema.js'sassembleTree/ContentDocument);src/test-support/is excluded from thetsdownbuild — onlysrc/index.tsandsrc/bin.tsare entry points — so none of it ever ships indist/.
Contributing
Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the monorepo root README for the release mechanism and CONTRIBUTING.md for the shared git hooks and history conventions. Work inside packages/document-mcp/.
License
MIT