# document-mcp

[![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/documents.js/tree/main/packages/document-mcp) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/document-mcp) [![npm version](https://img.shields.io/npm/v/document-mcp)](https://www.npmjs.com/package/document-mcp) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/documents.js/ci.yml?branch=main)](https://github.com/ExaDev/documents.js/actions)

> An MCP (Model Context Protocol) server exposing [`documents.js`](https://github.com/ExaDev/documents.js)'s document-conversion, `.odb`, metadata, and font tooling — plus [`document-compute.js`](../document-compute.js/README.md)'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`](../document-operations/README.md) `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`](../document-cli/README.md) 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.js/README.md#fidelity) documents, table included; it is not restated here.

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

```sh
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](#remote-transport-http) 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):

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

```sh
claude plugin marketplace add ExaDev/document-mcp
claude plugin install document-mcp@exadev
```

Or from within a running Claude Code session:

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

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

```toml
[mcp_servers.document-mcp]
command = "npx"
args = ["-y", "document-mcp"]
```

### Connecting from OpenCode

Add to `opencode.json`:

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

```json
{
  "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:

```json
{
  "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:

```sh
npx document-mcp --transport http --port 3000
```

This starts a real [Streamable HTTP](https://modelcontextprotocol.io) 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](https://nodejs.org/api/single-executable-applications.html) 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](https://github.com/ExaDev/documents.js/releases) 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](https://github.com/GoogleContainerTools/distroless) base rather than a Node install:

```sh
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](#remote-transport-http) 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`).

```sh
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](https://github.com/ExaDev/documents.js) — the library this server exposes.
- [document-outline.js](../document-outline.js/README.md) — the artefact-utilities package over document-schema.js's tree-form `DocumentTree` whose `buildOutline` powers `outline_document`.
- [document-compute.js](../document-compute.js/README.md) — the units-typed `MathExpression` evaluator whose `evaluate()` powers `compute_formula`.
- [document-cli](../document-cli/README.md) — the sibling CLI/TUI over the same library, whose toolchain this repository's scaffold mirrors.
- [Model Context Protocol](https://modelcontextprotocol.io) — the protocol this server implements, via [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server).

## Gotchas

- **Runtime dependencies are `document-operations` + `documents.js` + `@modelcontextprotocol/server` + `@modelcontextprotocol/node` + `zod` only; `pdf-codec`, `odf.js`, and `document-schema.js` are devDependencies (test-support only).** `@modelcontextprotocol/node` is the Node adapter (`toNodeHandler`) the `--transport http` listener uses to bridge `@modelcontextprotocol/server`'s web-standard `createMcpHandler` onto a plain `node:http` server — see [Remote transport](#remote-transport-http) above. Every tool's actual implementation — the conversion/editing/inspection/`.odb` logic previously defined directly in `src/tools/*.ts`, including its own reach into `document-outline.js`'s `buildOutline`/`outlineLeafText` and `document-compute.js`'s `evaluate()` — now lives in [`document-operations`](../document-operations/README.md) as a `DocumentOperation`; this package's own `src/tools/*.ts` are thin `registerOperation()` 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`'s `OdbReportNotSpecifiedError`, `odm_to_pdf`'s `OdmUnresolvedSectionError`), a `mapError` hook. Every runtime reach into `pdf-codec`/`odf.js` — `ProvidedFont`/`FontSubstitution`/`describeFontFace`/the `WinAnsi` substitution shape — goes through `documents.js`'s own re-exports (by way of `document-operations`), so a published install pulls in no direct `pdf-codec`/`odf.js` dependency. `odf.js` and `document-schema.js` survive in `devDependencies` solely for this package's own test fixtures and `compute_formula`'s own test assertions (`document-schema.js`'s `assembleTree`/`ContentDocument`); `src/test-support/` is excluded from the `tsdown` build — only `src/index.ts` and `src/bin.ts` are entry points — so none of it ever ships in `dist/`.

## Contributing

Release, CI, and commit-message conventions are all workspace-wide, not package-local — see the [monorepo root README](../../README.md#releases) for the release mechanism and [CONTRIBUTING.md](../../CONTRIBUTING.md) for the shared git hooks and history conventions. Work inside `packages/document-mcp/`.

## License

MIT
