Skip to main content
Glama

@awacloud/tool-convert

Document conversion (@awacloud/oconv) at the command line and over MCP. The CLI runs on bun, node and deno, from a repository checkout or from an installed copy (see "Running it" below); the MCP server runs on bun.

bun src/index.ts to-md <file> [--format <fmt>] [--at <iso>] [--out <file>]
bun src/index.ts from-md <file.md> --target <fmt> [--out <file>]
bun src/index.ts convert <file> --target <fmt> [--format <fmt>] [--out <file>]
bun src/index.ts to-html <file.md> [--out <file>]
bun src/index.ts --help | -h
bun src/index.ts --version | -v

Documentation: the package guide is docs/README.md, the full API reference (core functions, types, CLI verbs, MCP tools) is docs/api/, and changes are recorded in CHANGELOG.md.

Installation

npm install @awacloud/tool-convert

It has no bin field: run its entry file by path with bun, node or deno (see "Running it" below and "Exposed surface" at the end).

Related MCP server: GroupDocs.Conversion MCP Server

Quick Start

From the package directory, convert a document to Markdown, then the Markdown to PDF. report.docx below is a file you supply — the published package ships no sample document; from a repository checkout, the committed tests/fixtures/sample.docx works the same way and is what produced the output shown:

bun src/index.ts to-md report.docx --at 2026-01-01T00:00:00.000Z --out report.md
bun src/index.ts from-md report.md --target pdf --out report.pdf
# stderr: convert: 1 loss recorded   (the YAML front matter: frontmatter/stripped)

Or from code (the package's only import path is @awacloud/tool-convert, which resolves to src/core.ts; the snippet is TypeScript source consumed as is, so run it with bun):

import { readFileSync } from 'node:fs';
import { isCoreError, toMd } from '@awacloud/tool-convert';

const result = await toMd({
    name: 'report.docx',
    bytes: new Uint8Array(readFileSync('report.docx')),
    convertedAt: new Date().toISOString(),
});
if (isCoreError(result)) throw new Error(result.error);
console.log(result.lossy, result.markdown.split('\n').find((l) => l.startsWith('# ')));
// false # Sovereign RAG ingestion

Running it

The CLI is one file, src/index.ts, runnable directly by each runtime from a checkout with no build step and no adapter flags — everything runtime-specific (file I/O, argv, stdout/stderr, exit codes, SIGINT/SIGTERM) is consolidated in src/runtime.ts. Each command line below was actually run, not inferred:

# bun
bun src/index.ts to-md file.docx --out file.md

# node — no flags needed (node:fs/node:path/process/import.meta.main
# already give this file everything it uses)
node src/index.ts to-md file.docx --out file.md

# deno — two permissions, each because of a concrete call this CLI makes:
#   --allow-read   readFileSync() on the input file and on ../package.json
#   --allow-write  writeFileSync() when --out is given
# (measured: all four verbs run under --no-prompt with only these two)
deno run --allow-read --allow-write src/index.ts to-md file.docx --out file.md

No broader permission (-A/--allow-all, --allow-net) is needed or requested: this CLI never opens a socket.

Installed copy: the published package carries the TypeScript sources (src/) and a plain-JavaScript transpile of them (dist/, one file per source, produced when the package is exported and never committed, so a checkout has no dist/). Node 24.16.0 and Deno 2.8.3 refuse to execute TypeScript located under a node_modules directory (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING), so an installed copy runs through dist/ on those two and through src/ on bun:

node node_modules/@awacloud/tool-convert/dist/index.js to-md file.docx --out file.md
deno run --allow-read --allow-write node_modules/@awacloud/tool-convert/dist/index.js to-md file.docx --out file.md
bun node_modules/@awacloud/tool-convert/src/index.ts to-md file.docx --out file.md

import '@awacloud/tool-convert' picks the right file by itself: the exports map sends bun to src/core.ts and every other runtime to dist/core.js. Measured on an offline install of the packed tarball (and its packed dependencies) with bun 1.3.13, node 24.16.0 and deno 2.8.3.

Versions measured: bun 1.3.13, node 24.16.0, deno 2.8.3 (re-run 2026-10-03, Windows station). tests/runtime-matrix.integration.test.ts runs the same fixture through every runtime installed on the machine and self-skips the ones that are not (never reports a skipped runtime as passing) — see that file's module doc for exactly what byte-identity means for zip-container (docx/odt) targets, where @awacloud/oconv's zip writer stamps a wall-clock timestamp per entry (a pre-existing upstream non-determinism, not a runtime difference: two bun runs back to back already differ the same way). to-md, to-html and any pdf target are byte-identical outright; docx/odt targets are identical once that one known timestamp field is masked out.

SIGINT/SIGTERM: src/runtime.ts installs both handlers unconditionally on every runtime. The matrix test's signal-delivery checks self-skip on Windows (this station) because POSIX signal delivery to a child process via child_process.kill('SIGINT'|'SIGTERM') is documented as unreliable there for bun, node and deno alike — they were not exercised on this station; a POSIX host runs them for real.

Verbs

  • to-md <file> — any supported input format to structured Markdown (@awacloud/oconv's toMd). Supported input formats: docx, odt, xlsx, ods, pptx, odp, pdf.

  • from-md <file.md> --target <fmt> — Markdown to a target format (oconv.fromMd). Supported targets: docx, odt, pdf.

  • convert <file> --target <fmt> — a cross-format pair, direct (oconv.convert). Supported pairs: docx>odt, odt>docx, docx>pdf, odt>pdf — no other source/target combination is accepted, including any xlsx/ods/pptx/odp/pdf source and any same-format pair.

  • to-html <file.md> — Markdown to HTML through @awacloud/md's renderHtmlMod (via the md facade's .renderHtml) — a plain HTML fragment, no embedded CSS, no table of contents. Safe and sanitised: raw HTML is dropped (a raw block with its content, inline tags keeping their text), javascript:, vbscript:, file: and data: URLs (images included) become an empty href/src, anything outside the fw sanitiser allowlist is removed, and task-list checkboxes stay as disabled inputs keeping their checked state.

Format/target detection is by file extension, with an explicit --format override (to-md/convert) always winning. convert's target is never derived from <file>'s own name — it names the SOURCE, not the target — and is REQUIRED. Format/target/pair validation is @awacloud/oconv's own: this tool never duplicates it, it forwards the request and turns a thrown Error into the CLI's exit code + message.

--at <iso> (to-md only) pins the provenance timestamp that oconv.toMd's convertedAt requires — @awacloud/oconv never defaults it itself (reproducibility of the markdown output is caller-owned). Omit it for the current instant. It must be a strict RFC 3339 date-time (e.g. 2026-01-01T00:00:00.000Z, uppercase T/Z, calendar-valid, no leap second); anything else is a usage error (exit 2), checked before any file is opened. A valid value is kept verbatim, never normalised.

stdout / stderr discipline

With no --out, the converted bytes go to stdout so the tool composes in a shell pipeline. Every diagnostic, warning and the loss count go to stderr. A conversion that records fidelity losses still exits 0 — losses are output, not failure. With --out, the bytes are written to that file instead (never duplicated to stdout); the file's bytes are byte-identical to what stdout would have carried for the same input and --at. No ANSI colour escape is ever written to stdout.

Exit codes

Code

Meaning

0

success (including a lossy conversion); --help and --version always exit 0

1

generic error (unknown command, a non-usage runtime failure such as an unreadable input file)

2

usage/config error (missing command, missing file argument, missing --target, an unknown flag or a flag without its value, an invalid --at, an unsupported format/target/pair)

130

SIGINT

143

SIGTERM

Supported-format source of truth

The oconv facade of @awacloud/oconv is the single source of truth for which formats/targets/pairs are supported — this tool never hard-codes a second copy of the validation. It forwards the caller's file name and lets oconv detect the format/target itself, never pre-validating, and turns oconv's own unsupported format / unsupported target / unsupported pair errors into the CLI's exit code 2.

One caveat, stated plainly: oconv exposes no public constant listing those formats/targets/pairs — its SUPPORTED_FORMATS / SUPPORTED_TARGETS / SUPPORTED_PAIRS sets are declared inside its facade's factory closure and are not part of its published exports (the package root and its worker module only). The lists this tool's --help and error messages print (docx, odt, xlsx, ods, pptx, odp, pdf / docx, odt, pdf / the four cross-format pairs) are a maintained mirror of oconv's own sets and of its loss matrix and convert guide (linked under "Bounded claims" below) — for the human-facing hint only, never for gating a request. The mirror is exported as TO_MD_FORMATS, FROM_MD_TARGETS and CONVERT_PAIRS.

Bounded claims

This tool advertises no conversion pair @awacloud/oconv does not measurably deliver, and lossy pairs say so (via the stderr loss count) — see @awacloud/oconv's loss matrix for the fidelity table per pair, and its convert guide for the cross-format pairs.

MCP server

src/mcp.ts exposes the same four operations as a stdio, zero-dependency MCP server (newline-delimited JSON-RPC 2.0) — same transport discipline, same error shape, same "one tool call, one measured answer" contract as any other stdio MCP server, and fully self-contained.

bun src/mcp.ts

The server runs on bun only: it reads stdin through Bun.stdin, and under node 24.16.0 and deno 2.8.3 it starts, prints its stderr banner and exits 1 with convert-mcp: fatal: Bun is not defined (measured). It answers initialize, tools/list, tools/call, ping and the two notification methods; every other method returns METHOD_NOT_FOUND. It never throws across the transport: a bad argument or a failed conversion both come back as a tools/call result with isError: true and a structured { "error": "...", "usage": true|false } JSON payload — the same distinction src/core.ts and the CLI make between a usage/config error and a generic one.

Tools

Tool

Needs

Loses

to_md

path or (bytesBase64 + name); optional format, includeNotes, at, outPath

presentation/layout formatting and most inline styling beyond basic emphasis/headings — see the result's losses array

from_md

markdown or path; required target; optional name, outPath

nothing this step introduces on its own — only as rich as the input Markdown; see losses

convert

path or (bytesBase64 + name); required target; optional format, outPath

whatever the docx/odt/pdf loss matrix records for that pair — see losses; a pdf target is one-way

to_html

markdown or path; optional outPath

raw HTML (a block with its content; inline tags keep their text); javascript:, vbscript:, file: and data: URLs (images included) become an empty href/src; anything outside the sanitiser allowlist (task-list checkboxes stay, as disabled inputs); no embedded CSS, no table of contents, no footnotes, front matter or math; no losses field — the renderer does not itemise its drops

The tool names are the MCP tool registry of src/mcp.ts (TOOLS), not functions of src/core.ts: each tool calls the matching core function (toMd, fromMd, convert, toHtml).

Each tool's full description (returned by tools/list) restates its needs and losses in prose, and its inputSchema lists every property with a one-line description — a model can choose between the four without reading src/mcp.ts.

Loss data

to_md, from_md and convert are backed by @awacloud/oconv, which tracks fidelity losses per conversion: every successful result of these three carries losses (an array) and lossy (a boolean) from src/core.ts — inline with the content, or alongside the file-mode payload described below, never as a side channel a caller has to opt into. to_html carries no losses field: @awacloud/md's renderer drops content (raw HTML, javascript:, vbscript:, file: and data: URLs, anything outside the sanitiser allowlist) but does not itemise it, so there is no list to return — the absence is a limit of the renderer, not a lossless render.

Large-output rule

A conversion result can be far larger than a tool response should carry. Every tool accepts an optional outPath: when given, the output is always written there instead of returned inline (mirrors the CLI's --out). When outPath is omitted:

  • Text output (to_md's markdown, to_html's HTML) under MAX_INLINE_TEXT_CHARS (100,000 characters) comes back inline ("mode": "inline"). At or above that size, the full content is written to a fresh temp file and the result carries "mode": "file", outPath, length and an explicit preview (first 2,000 characters) plus a note stating the redirection — the preview is a clearly labelled excerpt, never a silent cut.

  • Binary output (from_md's and convert's bytes) under MAX_INLINE_BYTES (75,000 bytes) is base64-encoded inline ("mode": "inline", bytesBase64). At or above that size, the full bytes are written to a temp file and the result carries "mode": "file", outPath, bytesLength and a note — binary output is never truncated in place of redirection (a truncated document is a corrupt one), only redirected, in full.

Temp files created this way are not deleted by the server — the caller may not have read the path yet when the tool call returns; cleanup is the caller's or the OS's, same as any other tool that hands back a path.

Tests

The tests live in the repository only (the published package ships no test file). From the package directory:

bun test src/ tests/  # everything

Suite

Covers

src/core.test.ts

each core operation, the error-as-data contract, the Loss shape

src/descriptors.test.ts

the descriptor adapter: real oconv/md arrays pass, malformed entries fail by name

src/timestamp.test.ts

the strict RFC 3339 check behind --at and the at MCP argument

src/mcp.test.ts

MCP dispatch, schemas, large-output helpers

tests/cli.integration.test.ts

the CLI as a spawned process: streams, --out, exit codes

tests/mcp.integration.test.ts

a real stdio JSON-RPC session

tests/runtime-matrix.integration.test.ts

bun, node and deno give the same bytes and exit codes

tests/side-effects.integration.test.ts

importing the package has no side effect (sideEffects: false)

tests/docs-mirror.integration.test.ts

docs/api/ covers every export, verb and tool; doc links resolve

tests/shipped-surface.integration.test.ts

the shipped files carry no internal reference; their links resolve; this README's Installation, Licence and Project sections quote the manifest

Limitations

  • No Bun-compiled single executable — deferred to a future program for licence reasons. Node and deno run the CLI directly from source in a checkout, same as bun, and from dist/ in an installed copy (see "Running it" above).

  • The MCP server runs on bun only (see "MCP server" above).

  • The package declares no bin field, so installing it puts no command on the PATH.

Exposed surface

The package declares exactly one exports key, so its import surface is the single row below; the CLI and the MCP server are entry files you run by path, not exports.

Sub-path

Target

Usage

@awacloud/tool-convert

src/core.ts (bun), dist/core.js (node, deno and any other resolver)

the programmatic API: toMd, fromMd, convert, toHtml, isCoreError, the format-hint constants TO_MD_FORMATS, FROM_MD_TARGETS, CONVERT_PAIRS and the exported types — see docs/api/

Command entry points (files you run, never imported through the package name):

  • CLI — src/index.ts (dist/index.js for node and deno in an installed copy): the four verbs to-md, from-md, convert and to-html, plus --help and --version; invoked as "Running it" documents. Importing it runs nothing: the CLI body starts only when the file is the process entry point.

  • MCP server — src/mcp.ts: the stdio server described under "MCP server", started with bun src/mcp.ts.

The package's package.json declares no bin field: npm install creates no convert command, and the two files above are the whole command surface. Its main field names src/index.ts, but exports takes precedence, so import '@awacloud/tool-convert' resolves to src/core.ts on bun and to dist/core.js elsewhere. src/runtime.ts, src/descriptors.ts and src/timestamp.ts (and their dist/ transpiles) are internal modules of the two entry points, not part of the surface.

Maturity

L3, as declared in package.json awa.maturity. The package's own sources are held to a line-coverage floor of 0.92 by the repository coverage gate (awa.coverageFloor); the package itself is private and not yet published.

Licence

AGPL-3.0-only — see LICENSE in this package.

Copyright (c) 2026 AwaCloud SAS

Project

Related MCP Connectors

Related MCP Servers