document-mcp 4.4.3 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![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)
4
4
 
5
- > An MCP (Model Context Protocol) server exposing [`documents.js`](https://github.com/ExaDev/documents.js)'s document-conversion, `.odb`, metadata, and font tooling as MCP tools, so an MCP-speaking agent can convert, inspect, and edit docx/pptx/odt/odp/ods/odg/odf/pdf/odb/xlsx/markdown documents without writing TypeScript against `documents.js` directly.
5
+ > 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.
6
6
 
7
7
  `document-mcp` adds no conversion or editing logic of its own — it is a dispatch layer over `documents.js`'s existing conversion functions, `DocumentConverter` port, and `.odb`/PDF readers, wired up as MCP tools served over stdio. [`document-cli`](../document-cli/README.md) is the sibling frontend over the identical `documents.js` library — 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.
8
8
 
@@ -16,11 +16,15 @@ graph TD
16
16
  bytecodec("byte-codec")
17
17
  documents("documents.js")
18
18
  outline("document-outline.js")
19
+ compute("document-compute.js")
19
20
  mcp("document-mcp")
20
21
  cli("document-cli")
21
22
 
22
23
  schema --> outline
23
24
  outline --> mcp
25
+ schema --> compute
26
+ compute --> mcp
27
+ schema --> mcp
24
28
  schema --> ooxml
25
29
  schema --> odf
26
30
  schema --> pdfcodec
@@ -46,6 +50,7 @@ graph TD
46
50
  click bytecodec "https://github.com/ExaDev/documents.js/tree/main/packages/byte-codec" "byte-codec"
47
51
  click documents "https://github.com/ExaDev/documents.js" "documents.js"
48
52
  click outline "https://github.com/ExaDev/documents.js/tree/main/packages/document-outline.js" "document-outline.js"
53
+ click compute "https://github.com/ExaDev/documents.js/tree/main/packages/document-compute.js" "document-compute.js"
49
54
  click mcp "https://github.com/ExaDev/documents.js/tree/main/packages/document-mcp" "document-mcp"
50
55
  click cli "https://github.com/ExaDev/documents.js/tree/main/packages/document-cli" "document-cli"
51
56
 
@@ -174,9 +179,11 @@ Claude Web, Claude Mobile, and ChatGPT only accept **remote** (HTTP/SSE) MCP ser
174
179
  npx document-mcp --transport http --port 3000
175
180
  ```
176
181
 
177
- 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.
182
+ 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.
178
183
 
179
- > **Note:** the `--transport http` flag is not yet implemented. The server currently only supports stdio. Track this as a future capabilitythe MCP SDK supports SSE/streamable-http transports, so adding it is a matter of wiring the existing server to an HTTP listener.
184
+ 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.
185
+
186
+ > **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.
180
187
 
181
188
  ### Development
182
189
 
@@ -202,37 +209,39 @@ Every tool that takes or produces document bytes goes through the same two hybri
202
209
 
203
210
  ## Tools
204
211
 
205
- | Tool | Description |
206
- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
207
- | `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, and pdf. Not every `(source, targetFormat)` pair is direct; call `list_document_conversions` first. |
208
- | `list_document_conversions` | Lists every `(source, target)` format pair `convert_document` actually supports. |
209
- | `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. |
210
- | `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. |
211
- | `fonts` | Lists every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length). |
212
- | `describe_font_file` | Reads a standalone `.ttf`/`.otf` font file and reports the family/bold/italic triple it declares about itself. |
213
- | `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. |
214
- | `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. |
215
- | `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. |
216
- | `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`. |
217
- | `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`. |
218
- | `odb_tables` | Lists every table an embedded `.odb` database declares column names, types, and row dataacross every storage tier `documents.js` supports (HSQLDB TEXT/CACHED/BINARY, Firebird gbak backups). |
219
- | `odb_forms` | Lists every form an `.odb` database declares, with each form's own data source and field-bound controls. |
220
- | `odb_reports` | Lists every report an `.odb` database declares, with each report's own data-source command, band/group structure, and `rpt:` formula expressions. |
221
- | `odb_query` | Runs a bounded single-table `SELECT` over an embedded `.odb` database's extracted tables given directly as SQL or by naming a saved query. No database engine involved; an unsupported construct is reported as a tool error naming it, never silently ignored. |
222
- | `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. |
223
- | `odb_to_xlsx` | Extracts every table an embedded `.odb` database declares into one xlsx workbook, one sheet per table. |
224
- | `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. |
212
+ | Tool | Description |
213
+ | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
214
+ | `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, and pdf. Not every `(source, targetFormat)` pair is direct; call `list_document_conversions` first. |
215
+ | `list_document_conversions` | Lists every `(source, target)` format pair `convert_document` actually supports. |
216
+ | `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. |
217
+ | `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. |
218
+ | `fonts` | Lists every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length). |
219
+ | `describe_font_file` | Reads a standalone `.ttf`/`.otf` font file and reports the family/bold/italic triple it declares about itself. |
220
+ | `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. |
221
+ | `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. |
222
+ | `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. |
223
+ | `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`. |
224
+ | `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`. |
225
+ | `compute_formula` | Reads every formula a document embeds (docx/odt/markdown paragraphs and pptx/odp slide shapes; a table cell's own blocks toodocx'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. |
226
+ | `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). |
227
+ | `odb_forms` | Lists every form an `.odb` database declares, with each form's own data source and field-bound controls. |
228
+ | `odb_reports` | Lists every report an `.odb` database declares, with each report's own data-source command, band/group structure, and `rpt:` formula expressions. |
229
+ | `odb_query` | Runs a bounded single-table `SELECT` over an embedded `.odb` database's extracted tables — given directly as SQL or by naming a saved query. No database engine involved; an unsupported construct is reported as a tool error naming it, never silently ignored. |
230
+ | `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. |
231
+ | `odb_to_xlsx` | Extracts every table an embedded `.odb` database declares into one xlsx workbook, one sheet per table. |
232
+ | `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. |
225
233
 
226
234
  ## References
227
235
 
228
236
  - [documents.js](https://github.com/ExaDev/documents.js) — the library this server exposes.
229
237
  - [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`.
238
+ - [document-compute.js](../document-compute.js/README.md) — the units-typed `MathExpression` evaluator whose `evaluate()` powers `compute_formula`.
230
239
  - [document-cli](../document-cli/README.md) — the sibling CLI/TUI over the same library, whose toolchain this repository's scaffold mirrors.
231
240
  - [Model Context Protocol](https://modelcontextprotocol.io) — the protocol this server implements, via [`@modelcontextprotocol/server`](https://www.npmjs.com/package/@modelcontextprotocol/server).
232
241
 
233
242
  ## Gotchas
234
243
 
235
- - **Runtime dependencies are `documents.js` + `document-outline.js` + `@modelcontextprotocol/server` + `zod` only; `pdf-codec` and `odf.js` are devDependencies (test-support only).** `document-outline.js` is the one dependency beyond the server stack itself: `outline_document` imports `buildOutline`/`outlineLeafText` from it, and documents.js deliberately does not re-export them (the outline projection lives in the family's artefact-utilities package, which depends only on `document-schema.js` — already a transitive dependency via documents.js — so it adds no second copy of anything). Every runtime reach into `pdf-codec`/`odf.js` — `ProvidedFont`/`FontSubstitution`/`describeFontFace`/the `WinAnsi` substitution shape — goes through `documents.js`'s own re-exports, so a published install pulls in no direct `pdf-codec`/`odf.js` dependency. `odf.js` survives in `devDependencies` solely because `src/test-support/odm-fixture.ts` and `src/test-support/embedded-font-fixture.ts` build real ODF package fixtures from its low-level XML primitives (`zipPackage`/`el`/`rootElement`), and `src/test-support/` is excluded from the `tsdown` build — only `src/index.ts` and `src/bin.ts` are entry points — so neither fixture module ever ships in `dist/`.
244
+ - **Runtime dependencies are `documents.js` + `document-outline.js` + `document-compute.js` + `document-schema.js` + `@modelcontextprotocol/server` + `@modelcontextprotocol/node` + `zod` only; `pdf-codec` and `odf.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. `document-outline.js` is the one dependency beyond the server stack itself: `outline_document` imports `buildOutline`/`outlineLeafText` from it, and documents.js deliberately does not re-export them (the outline projection lives in the family's artefact-utilities package, which depends only on `document-schema.js` — already a transitive dependency via documents.js — so it adds no second copy of anything). `document-compute.js` and `document-schema.js` are the pair `compute_formula` needs for the identical reason: `evaluate()` lives in `document-compute.js` (documents.js has never depended on it, being the very gap [ExaDev/documents.js#928](https://github.com/ExaDev/documents.js/issues/928) closed), and documents.js re-exports neither `flattenTree` (the tree-to-flat transform turning `readNativeDocumentTree`'s `DocumentTree` back into the `ContentDocument` a formula walk needs) nor the `ContentFormula`/`MathExpression`/`FormulaBindings`/`Quantity`/`Interval`/`SymbolTable` types `evaluate()`'s own inputs and outputs are shaped by — both come from `document-schema.js` directly, exactly as that package's own README already documents for any `evaluate()` caller. Every runtime reach into `pdf-codec`/`odf.js` — `ProvidedFont`/`FontSubstitution`/`describeFontFace`/the `WinAnsi` substitution shape — goes through `documents.js`'s own re-exports, so a published install pulls in no direct `pdf-codec`/`odf.js` dependency. `odf.js` survives in `devDependencies` solely because `src/test-support/odm-fixture.ts`, `src/test-support/embedded-font-fixture.ts`, and `src/test-support/odf-formula-fixture.ts` build real ODF package fixtures from its low-level XML primitives (`zipPackage`/`el`/`rootElement`), and `src/test-support/` is excluded from the `tsdown` build — only `src/index.ts` and `src/bin.ts` are entry points — so none of these fixture modules ever ships in `dist/`.
236
245
 
237
246
  ## Contributing
238
247
 
package/dist/bin.js CHANGED
@@ -1,14 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
3
- import { McpServer } from "@modelcontextprotocol/server";
4
- import { DocumentFormatSchema, OdbReportNotSpecifiedError, OdmUnresolvedSectionError, UnrecognizedDocumentSchemaError, base64ToBytes, buildDocumentBytes, bytesToBase64, createLocalDocumentConverter, decodeOdbPackage, decodePackage, describeFontFace, documentFromJson, documentSchemaKindOf, evaluateSelect, extractSourceFontsForFormat, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocumentMetadata, readDocxExtras, readNativeDocumentTree, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, setDocumentMetadata } from "documents.js";
3
+ import { createServer } from "node:http";
4
+ import { toNodeHandler } from "@modelcontextprotocol/node";
5
+ import { McpServer, createMcpHandler } from "@modelcontextprotocol/server";
6
+ import { evaluate } from "document-compute.js";
7
+ import { ContentDocumentSchema, EvaluationValueSchema, FormulaBindingsSchema, flattenTree } from "document-schema.js";
8
+ import { DocumentFormatSchema, OdbReportNotSpecifiedError, OdmUnresolvedSectionError, UnrecognizedDocumentSchemaError, base64ToBytes, buildDocumentBytes, bytesToBase64, collectDocumentFormulas, createLocalDocumentConverter, decodeOdbPackage, decodePackage, describeFontFace, documentFromJson, documentSchemaKindOf, evaluateSelect, extractSourceFontsForFormat, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocumentMetadata, readDocxExtras, readNativeDocumentTree, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, setDocumentMetadata } from "documents.js";
5
9
  import { z } from "zod";
6
10
  import { readFile, writeFile } from "node:fs/promises";
7
11
  import { existsSync, readFileSync } from "node:fs";
8
12
  import { basename, join } from "node:path";
9
13
  import { buildOutline, isOutlineNode, outlineLeafText } from "document-outline.js";
10
14
  //#region package.json
11
- var version = "4.4.3";
15
+ var version = "4.5.1";
12
16
  //#endregion
13
17
  //#region src/io/document-input.ts
14
18
  const EXTENSION_TO_FORMAT = {
@@ -72,6 +76,99 @@ async function resolveDocumentInput(input, options) {
72
76
  };
73
77
  }
74
78
  //#endregion
79
+ //#region src/tools/compute-formula.ts
80
+ function toErrorResult$2(error) {
81
+ return {
82
+ content: [{
83
+ type: "text",
84
+ text: error instanceof Error ? error.message : String(error)
85
+ }],
86
+ isError: true
87
+ };
88
+ }
89
+ function evaluateFormula(formula, bindings, symbolTable) {
90
+ if (formula.content === void 0) return { status: "no-content" };
91
+ try {
92
+ return {
93
+ status: "evaluated",
94
+ result: evaluate(formula.content, bindings, symbolTable)
95
+ };
96
+ } catch (error) {
97
+ return {
98
+ status: "error",
99
+ errorType: error instanceof Error ? error.name : String(error),
100
+ message: error instanceof Error ? error.message : String(error)
101
+ };
102
+ }
103
+ }
104
+ const FormulaOutcomeSchema = z.union([
105
+ z.object({
106
+ status: z.literal("evaluated"),
107
+ result: EvaluationValueSchema
108
+ }),
109
+ z.object({ status: z.literal("no-content") }),
110
+ z.object({
111
+ status: z.literal("error"),
112
+ errorType: z.string(),
113
+ message: z.string()
114
+ })
115
+ ]);
116
+ const documentKindValues = ContentDocumentSchema.options.map((option) => option.shape.kind.value);
117
+ const ComputeFormulaOutputSchema = z.object({
118
+ sourceFormat: DocumentFormatSchema,
119
+ documentKind: z.enum(documentKindValues),
120
+ formulaCount: z.number(),
121
+ formulas: z.array(z.object({
122
+ index: z.number(),
123
+ sourcePath: z.string().optional(),
124
+ locate: z.string(),
125
+ latex: z.string().optional(),
126
+ outcome: FormulaOutcomeSchema
127
+ }))
128
+ });
129
+ function registerComputeFormulaTools(server) {
130
+ server.registerTool("compute_formula", {
131
+ title: "Compute document formulas",
132
+ description: "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 (e.g. F = m * a) needs that symbol's value supplied via bindings, keyed by the document's own symbol-table id (see the document's symbolTable.symbols[].id, or a returned formula's own latex to identify which symbol is which); a formula with no free symbols (units and numeric literals only) evaluates with no bindings at all. Each formula reports its own outcome independently -- 'evaluated' with the result, 'no-content' when the formula was never lowered to a computable MathExpression (the common case for a spreadsheet's own embedded formula object, or any format other than markdown, none of which yet stores the semantic layer on disk), or 'error' naming which document-compute.js error it hit (e.g. UnboundSymbolError) -- so one formula needing more bindings never blocks the others. Each entry's own `locate` field is the reliable way to tell two formulas in the same document apart -- a structural path guaranteed unique per formula -- unlike `sourcePath`, which several formats leave undefined or stamp with the identical constant across sibling formulas (markdown's own display-math lowering among them).",
133
+ inputSchema: z.object({
134
+ source: DocumentInputSchema.describe("The document to read formulas from."),
135
+ bindings: FormulaBindingsSchema.optional().describe("Known values for symbols the document's formulas reference, keyed by symbol-table id (a Quantity { kind: 'quantity', magnitude, dimension } or an Interval { kind: 'interval', min, max, dimension } per symbol). Omit for a document whose formulas are fully closed (units and numeric literals only, no 'sym' nodes).")
136
+ }),
137
+ outputSchema: ComputeFormulaOutputSchema
138
+ }, async ({ source, bindings }, ctx) => {
139
+ const { signal } = ctx.mcpReq;
140
+ try {
141
+ const { bytes, format } = await resolveDocumentInput(source, { signal });
142
+ const tree = readNativeDocumentTree(format, bytes, { signal });
143
+ const document = flattenTree(tree);
144
+ const entries = collectDocumentFormulas(document);
145
+ const resolvedBindings = bindings ?? {};
146
+ const formulas = entries.map((entry, index) => ({
147
+ index,
148
+ sourcePath: entry.sourcePath,
149
+ locate: entry.locate,
150
+ latex: entry.formula.presentation?.latex,
151
+ outcome: evaluateFormula(entry.formula, resolvedBindings, entry.symbolTable)
152
+ }));
153
+ const structuredContent = {
154
+ sourceFormat: format,
155
+ documentKind: document.kind,
156
+ formulaCount: formulas.length,
157
+ formulas
158
+ };
159
+ return {
160
+ content: [{
161
+ type: "text",
162
+ text: JSON.stringify(structuredContent)
163
+ }],
164
+ structuredContent
165
+ };
166
+ } catch (error) {
167
+ return toErrorResult$2(error);
168
+ }
169
+ });
170
+ }
171
+ //#endregion
75
172
  //#region src/io/document-output.ts
76
173
  /**
77
174
  * The hybrid output shape every MCP tool that produces a document accepts: an optional filesystem path to write the result to. Omitting it returns the bytes inline, base64-encoded, instead.
@@ -871,11 +968,12 @@ function registerPdfInspectTools(server) {
871
968
  }
872
969
  //#endregion
873
970
  //#region src/server.ts
874
- function createServer() {
971
+ function createServer$1() {
875
972
  const server = new McpServer({
876
973
  name: "document-mcp",
877
974
  version
878
975
  });
976
+ registerComputeFormulaTools(server);
879
977
  registerConvertTools(server);
880
978
  registerDocxExtrasTools(server);
881
979
  registerFontTools(server);
@@ -889,7 +987,60 @@ function createServer() {
889
987
  return server;
890
988
  }
891
989
  //#endregion
990
+ //#region src/serve-http.ts
991
+ const MCP_HTTP_PATH = "/mcp";
992
+ function serveHttp(port) {
993
+ const handler = createMcpHandler(createServer$1);
994
+ const nodeHandler = toNodeHandler(handler);
995
+ const httpServer = createServer((req, res) => {
996
+ if (req.url === void 0) throw new Error("serveHttp: request has no url");
997
+ if (new URL(req.url, "http://localhost").pathname !== "/mcp") {
998
+ res.writeHead(404, { "content-type": "text/plain" }).end("Not found");
999
+ return;
1000
+ }
1001
+ nodeHandler(req, res);
1002
+ });
1003
+ return new Promise((resolve) => {
1004
+ httpServer.listen(port, "127.0.0.1", () => {
1005
+ resolve(httpServer);
1006
+ });
1007
+ });
1008
+ }
1009
+ //#endregion
892
1010
  //#region src/bin.ts
893
- serveStdio(createServer);
1011
+ const DEFAULT_HTTP_PORT = 3e3;
1012
+ function readFlag(args, name) {
1013
+ const prefix = `--${name}=`;
1014
+ for (const [index, arg] of args.entries()) {
1015
+ if (arg.startsWith(prefix)) return arg.slice(prefix.length);
1016
+ if (arg === `--${name}`) {
1017
+ const value = args[index + 1];
1018
+ if (value === void 0) throw new Error(`--${name} requires a value`);
1019
+ return value;
1020
+ }
1021
+ }
1022
+ }
1023
+ function parsePort(raw) {
1024
+ const port = Number.parseInt(raw, 10);
1025
+ if (!Number.isInteger(port) || String(port) !== raw.trim() || port < 0 || port > 65535) throw new Error(`--port must be an integer between 0 and 65535, got "${raw}"`);
1026
+ return port;
1027
+ }
1028
+ async function main() {
1029
+ const args = process.argv.slice(2);
1030
+ const transport = readFlag(args, "transport") ?? "stdio";
1031
+ if (transport === "stdio") {
1032
+ serveStdio(createServer$1);
1033
+ return;
1034
+ }
1035
+ if (transport === "http") {
1036
+ const portArg = readFlag(args, "port");
1037
+ const address = (await serveHttp(portArg === void 0 ? DEFAULT_HTTP_PORT : parsePort(portArg))).address();
1038
+ if (address === null || typeof address === "string") throw new Error("Expected the HTTP server to bind a TCP address, not a pipe or Unix socket");
1039
+ console.error(`document-mcp listening on http://127.0.0.1:${String(address.port)}${MCP_HTTP_PATH}`);
1040
+ return;
1041
+ }
1042
+ throw new Error(`Unknown --transport "${transport}". Supported values: stdio, http.`);
1043
+ }
1044
+ await main();
894
1045
  //#endregion
895
1046
  export {};
package/dist/index.cjs CHANGED
@@ -1,5 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let _modelcontextprotocol_server = require("@modelcontextprotocol/server");
3
+ let document_compute_js = require("document-compute.js");
4
+ let document_schema_js = require("document-schema.js");
3
5
  let documents_js = require("documents.js");
4
6
  let zod = require("zod");
5
7
  let node_fs_promises = require("node:fs/promises");
@@ -7,7 +9,7 @@ let node_fs = require("node:fs");
7
9
  let node_path = require("node:path");
8
10
  let document_outline_js = require("document-outline.js");
9
11
  //#region package.json
10
- var version = "4.4.3";
12
+ var version = "4.5.1";
11
13
  //#endregion
12
14
  //#region src/io/document-input.ts
13
15
  const EXTENSION_TO_FORMAT = {
@@ -71,6 +73,99 @@ async function resolveDocumentInput(input, options) {
71
73
  };
72
74
  }
73
75
  //#endregion
76
+ //#region src/tools/compute-formula.ts
77
+ function toErrorResult$2(error) {
78
+ return {
79
+ content: [{
80
+ type: "text",
81
+ text: error instanceof Error ? error.message : String(error)
82
+ }],
83
+ isError: true
84
+ };
85
+ }
86
+ function evaluateFormula(formula, bindings, symbolTable) {
87
+ if (formula.content === void 0) return { status: "no-content" };
88
+ try {
89
+ return {
90
+ status: "evaluated",
91
+ result: (0, document_compute_js.evaluate)(formula.content, bindings, symbolTable)
92
+ };
93
+ } catch (error) {
94
+ return {
95
+ status: "error",
96
+ errorType: error instanceof Error ? error.name : String(error),
97
+ message: error instanceof Error ? error.message : String(error)
98
+ };
99
+ }
100
+ }
101
+ const FormulaOutcomeSchema = zod.z.union([
102
+ zod.z.object({
103
+ status: zod.z.literal("evaluated"),
104
+ result: document_schema_js.EvaluationValueSchema
105
+ }),
106
+ zod.z.object({ status: zod.z.literal("no-content") }),
107
+ zod.z.object({
108
+ status: zod.z.literal("error"),
109
+ errorType: zod.z.string(),
110
+ message: zod.z.string()
111
+ })
112
+ ]);
113
+ const documentKindValues = document_schema_js.ContentDocumentSchema.options.map((option) => option.shape.kind.value);
114
+ const ComputeFormulaOutputSchema = zod.z.object({
115
+ sourceFormat: documents_js.DocumentFormatSchema,
116
+ documentKind: zod.z.enum(documentKindValues),
117
+ formulaCount: zod.z.number(),
118
+ formulas: zod.z.array(zod.z.object({
119
+ index: zod.z.number(),
120
+ sourcePath: zod.z.string().optional(),
121
+ locate: zod.z.string(),
122
+ latex: zod.z.string().optional(),
123
+ outcome: FormulaOutcomeSchema
124
+ }))
125
+ });
126
+ function registerComputeFormulaTools(server) {
127
+ server.registerTool("compute_formula", {
128
+ title: "Compute document formulas",
129
+ description: "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 (e.g. F = m * a) needs that symbol's value supplied via bindings, keyed by the document's own symbol-table id (see the document's symbolTable.symbols[].id, or a returned formula's own latex to identify which symbol is which); a formula with no free symbols (units and numeric literals only) evaluates with no bindings at all. Each formula reports its own outcome independently -- 'evaluated' with the result, 'no-content' when the formula was never lowered to a computable MathExpression (the common case for a spreadsheet's own embedded formula object, or any format other than markdown, none of which yet stores the semantic layer on disk), or 'error' naming which document-compute.js error it hit (e.g. UnboundSymbolError) -- so one formula needing more bindings never blocks the others. Each entry's own `locate` field is the reliable way to tell two formulas in the same document apart -- a structural path guaranteed unique per formula -- unlike `sourcePath`, which several formats leave undefined or stamp with the identical constant across sibling formulas (markdown's own display-math lowering among them).",
130
+ inputSchema: zod.z.object({
131
+ source: DocumentInputSchema.describe("The document to read formulas from."),
132
+ bindings: document_schema_js.FormulaBindingsSchema.optional().describe("Known values for symbols the document's formulas reference, keyed by symbol-table id (a Quantity { kind: 'quantity', magnitude, dimension } or an Interval { kind: 'interval', min, max, dimension } per symbol). Omit for a document whose formulas are fully closed (units and numeric literals only, no 'sym' nodes).")
133
+ }),
134
+ outputSchema: ComputeFormulaOutputSchema
135
+ }, async ({ source, bindings }, ctx) => {
136
+ const { signal } = ctx.mcpReq;
137
+ try {
138
+ const { bytes, format } = await resolveDocumentInput(source, { signal });
139
+ const tree = (0, documents_js.readNativeDocumentTree)(format, bytes, { signal });
140
+ const document = (0, document_schema_js.flattenTree)(tree);
141
+ const entries = (0, documents_js.collectDocumentFormulas)(document);
142
+ const resolvedBindings = bindings ?? {};
143
+ const formulas = entries.map((entry, index) => ({
144
+ index,
145
+ sourcePath: entry.sourcePath,
146
+ locate: entry.locate,
147
+ latex: entry.formula.presentation?.latex,
148
+ outcome: evaluateFormula(entry.formula, resolvedBindings, entry.symbolTable)
149
+ }));
150
+ const structuredContent = {
151
+ sourceFormat: format,
152
+ documentKind: document.kind,
153
+ formulaCount: formulas.length,
154
+ formulas
155
+ };
156
+ return {
157
+ content: [{
158
+ type: "text",
159
+ text: JSON.stringify(structuredContent)
160
+ }],
161
+ structuredContent
162
+ };
163
+ } catch (error) {
164
+ return toErrorResult$2(error);
165
+ }
166
+ });
167
+ }
168
+ //#endregion
74
169
  //#region src/io/document-output.ts
75
170
  /**
76
171
  * The hybrid output shape every MCP tool that produces a document accepts: an optional filesystem path to write the result to. Omitting it returns the bytes inline, base64-encoded, instead.
@@ -875,6 +970,7 @@ function createServer() {
875
970
  name: "document-mcp",
876
971
  version
877
972
  });
973
+ registerComputeFormulaTools(server);
878
974
  registerConvertTools(server);
879
975
  registerDocxExtrasTools(server);
880
976
  registerFontTools(server);
package/dist/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
- import { DocumentFormatSchema, OdbReportNotSpecifiedError, OdmUnresolvedSectionError, UnrecognizedDocumentSchemaError, base64ToBytes, buildDocumentBytes, bytesToBase64, createLocalDocumentConverter, decodeOdbPackage, decodePackage, describeFontFace, documentFromJson, documentSchemaKindOf, evaluateSelect, extractSourceFontsForFormat, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocumentMetadata, readDocxExtras, readNativeDocumentTree, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, setDocumentMetadata } from "documents.js";
2
+ import { evaluate } from "document-compute.js";
3
+ import { ContentDocumentSchema, EvaluationValueSchema, FormulaBindingsSchema, flattenTree } from "document-schema.js";
4
+ import { DocumentFormatSchema, OdbReportNotSpecifiedError, OdmUnresolvedSectionError, UnrecognizedDocumentSchemaError, base64ToBytes, buildDocumentBytes, bytesToBase64, collectDocumentFormulas, createLocalDocumentConverter, decodeOdbPackage, decodePackage, describeFontFace, documentFromJson, documentSchemaKindOf, evaluateSelect, extractSourceFontsForFormat, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, parseSelect, readDocumentMetadata, readDocxExtras, readNativeDocumentTree, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, setDocumentMetadata } from "documents.js";
3
5
  import { z } from "zod";
4
6
  import { readFile, writeFile } from "node:fs/promises";
5
7
  import { existsSync, readFileSync } from "node:fs";
6
8
  import { basename, join } from "node:path";
7
9
  import { buildOutline, isOutlineNode, outlineLeafText } from "document-outline.js";
8
10
  //#region package.json
9
- var version = "4.4.3";
11
+ var version = "4.5.1";
10
12
  //#endregion
11
13
  //#region src/io/document-input.ts
12
14
  const EXTENSION_TO_FORMAT = {
@@ -70,6 +72,99 @@ async function resolveDocumentInput(input, options) {
70
72
  };
71
73
  }
72
74
  //#endregion
75
+ //#region src/tools/compute-formula.ts
76
+ function toErrorResult$2(error) {
77
+ return {
78
+ content: [{
79
+ type: "text",
80
+ text: error instanceof Error ? error.message : String(error)
81
+ }],
82
+ isError: true
83
+ };
84
+ }
85
+ function evaluateFormula(formula, bindings, symbolTable) {
86
+ if (formula.content === void 0) return { status: "no-content" };
87
+ try {
88
+ return {
89
+ status: "evaluated",
90
+ result: evaluate(formula.content, bindings, symbolTable)
91
+ };
92
+ } catch (error) {
93
+ return {
94
+ status: "error",
95
+ errorType: error instanceof Error ? error.name : String(error),
96
+ message: error instanceof Error ? error.message : String(error)
97
+ };
98
+ }
99
+ }
100
+ const FormulaOutcomeSchema = z.union([
101
+ z.object({
102
+ status: z.literal("evaluated"),
103
+ result: EvaluationValueSchema
104
+ }),
105
+ z.object({ status: z.literal("no-content") }),
106
+ z.object({
107
+ status: z.literal("error"),
108
+ errorType: z.string(),
109
+ message: z.string()
110
+ })
111
+ ]);
112
+ const documentKindValues = ContentDocumentSchema.options.map((option) => option.shape.kind.value);
113
+ const ComputeFormulaOutputSchema = z.object({
114
+ sourceFormat: DocumentFormatSchema,
115
+ documentKind: z.enum(documentKindValues),
116
+ formulaCount: z.number(),
117
+ formulas: z.array(z.object({
118
+ index: z.number(),
119
+ sourcePath: z.string().optional(),
120
+ locate: z.string(),
121
+ latex: z.string().optional(),
122
+ outcome: FormulaOutcomeSchema
123
+ }))
124
+ });
125
+ function registerComputeFormulaTools(server) {
126
+ server.registerTool("compute_formula", {
127
+ title: "Compute document formulas",
128
+ description: "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 (e.g. F = m * a) needs that symbol's value supplied via bindings, keyed by the document's own symbol-table id (see the document's symbolTable.symbols[].id, or a returned formula's own latex to identify which symbol is which); a formula with no free symbols (units and numeric literals only) evaluates with no bindings at all. Each formula reports its own outcome independently -- 'evaluated' with the result, 'no-content' when the formula was never lowered to a computable MathExpression (the common case for a spreadsheet's own embedded formula object, or any format other than markdown, none of which yet stores the semantic layer on disk), or 'error' naming which document-compute.js error it hit (e.g. UnboundSymbolError) -- so one formula needing more bindings never blocks the others. Each entry's own `locate` field is the reliable way to tell two formulas in the same document apart -- a structural path guaranteed unique per formula -- unlike `sourcePath`, which several formats leave undefined or stamp with the identical constant across sibling formulas (markdown's own display-math lowering among them).",
129
+ inputSchema: z.object({
130
+ source: DocumentInputSchema.describe("The document to read formulas from."),
131
+ bindings: FormulaBindingsSchema.optional().describe("Known values for symbols the document's formulas reference, keyed by symbol-table id (a Quantity { kind: 'quantity', magnitude, dimension } or an Interval { kind: 'interval', min, max, dimension } per symbol). Omit for a document whose formulas are fully closed (units and numeric literals only, no 'sym' nodes).")
132
+ }),
133
+ outputSchema: ComputeFormulaOutputSchema
134
+ }, async ({ source, bindings }, ctx) => {
135
+ const { signal } = ctx.mcpReq;
136
+ try {
137
+ const { bytes, format } = await resolveDocumentInput(source, { signal });
138
+ const tree = readNativeDocumentTree(format, bytes, { signal });
139
+ const document = flattenTree(tree);
140
+ const entries = collectDocumentFormulas(document);
141
+ const resolvedBindings = bindings ?? {};
142
+ const formulas = entries.map((entry, index) => ({
143
+ index,
144
+ sourcePath: entry.sourcePath,
145
+ locate: entry.locate,
146
+ latex: entry.formula.presentation?.latex,
147
+ outcome: evaluateFormula(entry.formula, resolvedBindings, entry.symbolTable)
148
+ }));
149
+ const structuredContent = {
150
+ sourceFormat: format,
151
+ documentKind: document.kind,
152
+ formulaCount: formulas.length,
153
+ formulas
154
+ };
155
+ return {
156
+ content: [{
157
+ type: "text",
158
+ text: JSON.stringify(structuredContent)
159
+ }],
160
+ structuredContent
161
+ };
162
+ } catch (error) {
163
+ return toErrorResult$2(error);
164
+ }
165
+ });
166
+ }
167
+ //#endregion
73
168
  //#region src/io/document-output.ts
74
169
  /**
75
170
  * The hybrid output shape every MCP tool that produces a document accepts: an optional filesystem path to write the result to. Omitting it returns the bytes inline, base64-encoded, instead.
@@ -874,6 +969,7 @@ function createServer() {
874
969
  name: "document-mcp",
875
970
  version
876
971
  });
972
+ registerComputeFormulaTools(server);
877
973
  registerConvertTools(server);
878
974
  registerDocxExtrasTools(server);
879
975
  registerFontTools(server);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-mcp",
3
- "version": "4.4.3",
3
+ "version": "4.5.1",
4
4
  "description": "MCP (Model Context Protocol) server exposing documents.js's document-conversion, .odb, metadata, and font tooling as MCP tools.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -86,9 +86,12 @@
86
86
  "license": "MIT",
87
87
  "packageManager": "pnpm@11.6.0",
88
88
  "dependencies": {
89
+ "@modelcontextprotocol/node": "^2.0.0",
89
90
  "@modelcontextprotocol/server": "^2.0.0",
90
- "document-outline.js": "^3.2.5",
91
- "documents.js": "^6.5.3",
91
+ "document-compute.js": "^1.2.21",
92
+ "document-outline.js": "^3.3.0",
93
+ "document-schema.js": "^6.0.0",
94
+ "documents.js": "^7.0.1",
92
95
  "zod": "^4.2.0"
93
96
  },
94
97
  "devDependencies": {
@@ -99,13 +102,13 @@
99
102
  "@vitest/coverage-v8": "^4.1.10",
100
103
  "eslint": "^10.8.0",
101
104
  "husky": "^9.1.7",
102
- "odf.js": "^6.4.1",
105
+ "odf.js": "^7.0.0",
103
106
  "publint": "^0.3.22",
104
107
  "semantic-release": "^25.0.8",
105
108
  "tsdown": "^0.22.14",
106
109
  "turbo": "^2.10.8",
107
110
  "typescript": "^6.0.3",
108
111
  "vitest": "^4.1.10",
109
- "wpd-codec": "^1.1.3"
112
+ "wpd-codec": "^2.0.0"
110
113
  }
111
114
  }