@awacloud/tool-convert converts office documents to and from Markdown.
It has three faces over one core:
- a command-line tool (
src/index.ts); - a stdio MCP server (
src/mcp.ts) that exposes the same operations to an AI client; - a programmatic core (
src/core.ts, the package's.export).
The conversions themselves are done by @awacloud/oconv, and the HTML
rendering by @awacloud/md. This package adds no conversion logic of its
own. It adds file and stream handling, exit codes, and the MCP transport.
Runtimes
| Face | Bun | Node | Deno |
|---|---|---|---|
CLI (src/index.ts) |
✅ 1.3.13 | ✅ 24.16.0 | ✅ 2.8.3 |
Core (@awacloud/tool-convert) |
✅ src/core.ts |
✅ dist/core.js |
✅ dist/core.js |
MCP server (src/mcp.ts) |
✅ | — | — |
The versions are the ones measured. The package's engines field declares
them as minimums. In a repository checkout every runtime runs src/. The
published package also carries dist/, a plain-JavaScript transpile of
src/ produced when the package is exported: Node and Deno refuse to run
TypeScript under node_modules, so an installed copy runs dist/ on them,
and the package's exports map picks src/ for Bun and dist/ for the
rest. tests/runtime-matrix.integration.test.ts runs the same
fixture through every runtime installed on the machine and checks that the
output bytes match. For docx and odt output it first masks the zip
timestamp fields, which change on every run.
The MCP server reads its input through Bun.stdin, so it runs on Bun only.
On Deno the CLI needs two permissions: --allow-read for the input file
and package.json, and --allow-write for --out. All four verbs were
measured to run on Deno 2.8.3 under --no-prompt with only those two. The
CLI never opens a socket.
deno run --allow-read --allow-write src/index.ts to-md report.docx --out report.md
Operations
| Operation | CLI verb | MCP tool | Core function | Source → target |
|---|---|---|---|---|
| Document to Markdown | to-md |
to_md |
toMd |
docx, odt, xlsx, ods, pptx, odp, pdf → Markdown |
| Markdown to document | from-md |
from_md |
fromMd |
Markdown → docx, odt, pdf |
| Document to document | convert |
convert |
convert |
docx>odt, odt>docx, docx>pdf, odt>pdf |
| Markdown to HTML | to-html |
to_html |
toHtml |
Markdown → HTML fragment |
The full reference is in api/.
to-html emits a plain HTML fragment. It has no embedded CSS and no table
of contents.
The loss ledger
A conversion can lose something: a raw HTML block that Word has no place
for, an image whose bytes are not available, a task-list checkbox. Every
@awacloud/oconv-backed result says so. It never drops content silently.
lossesis an array ofLossrecords, in document order: reader losses first, then writer losses.lossyistrueexactly whenlossesis not empty.- A lossy conversion is still a success. The CLI exits
0and prints the count on stderr (convert: 1 loss recorded). The MCP result carries the full array next to the output. to-htmlhas no ledger. Its render drops raw HTML and unsafe URLs but does not itemise them, so there is nothing it can list — seetoHtml.
Measured example: a task-list item going to docx.
{ "losses": [{ "code": "list/task-marker-dropped", "detail": "[x]" }], "lossy": true }
@awacloud/oconv's docs/loss-matrix.md gives the fidelity table for each
format pair.
Errors
The core never throws. Each operation returns either its output
(error: null) or a CoreError
{ error, usage }. usage: true marks a caller mistake, such as an
unsupported format, target or pair. The CLI maps it to exit code 2 and
every other failure to 1. The MCP server returns both as an
isError: true result carrying the same payload.