document-mcp 4.9.2 → 4.10.0

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/dist/index.js CHANGED
@@ -1,83 +1,11 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
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, createDocx, createLocalDocumentConverter, createMarkdownEditor, createOdg, createOdp, createOds, createOdt, createPdf, createPptx, decodeMarkdownText, decodeOdbPackage, decodePackage, describeFontFace, documentFromJson, documentSchemaKindOf, encodeMarkdownText, evaluateSelect, extractSourceFontsForFormat, odbReportToDocx, odbReportToOdt, odbReportToPdf, odbToCsv, odbToXlsx, odmToPdf, openDocx, openMarkdown, openOdt, parseSelect, readDocumentMetadata, readDocxExtras, readNativeDocumentTree, readOdbForms, readOdbInventory, readOdbReportContent, readOdbReports, readOdbTables, readPdf, rgbHexToColor, setDocumentMetadata } from "documents.js";
5
- import { z } from "zod";
6
- import { readFile, writeFile } from "node:fs/promises";
7
- import { existsSync, readFileSync } from "node:fs";
8
- import { basename, join } from "node:path";
9
- import { buildOutline, isOutlineNode, outlineLeafText } from "document-outline.js";
2
+ import { computeFormulaOperation, convertDocumentOperation, describeFontFileOperation, documentAppendParagraphsOperation, documentCreateOperation, docxExtrasOperation, fontsOperation, fromPackageOperation, listDocumentConversionsOperation, metadataReadOperation, metadataWriteOperation, odbFormsOperation, odbQueryOperation, odbRenderReportOperation, odbReportsOperation, odbTablesOperation, odbToCsvOperation, odbToXlsxOperation, odmToPdfOperation, outlineDocumentOperation, pdfInspectOperation } from "document-operations";
3
+ import { OdbReportNotSpecifiedError, OdmUnresolvedSectionError } from "documents.js";
10
4
  //#region package.json
11
- var version = "4.9.2";
5
+ var version = "4.10.0";
12
6
  //#endregion
13
- //#region src/io/document-input.ts
14
- const EXTENSION_TO_FORMAT = {
15
- docx: "docx",
16
- dotx: "docx",
17
- docm: "docx",
18
- pptx: "pptx",
19
- potx: "pptx",
20
- pptm: "pptx",
21
- xlsx: "xlsx",
22
- xltx: "xlsx",
23
- xlsm: "xlsx",
24
- odt: "odt",
25
- ott: "odt",
26
- odp: "odp",
27
- otp: "odp",
28
- ods: "ods",
29
- ots: "ods",
30
- odg: "odg",
31
- otg: "odg",
32
- odf: "odf",
33
- otf: "odf",
34
- markdown: "markdown",
35
- md: "markdown",
36
- rtf: "rtf",
37
- wpd: "wpd",
38
- doc: "doc",
39
- xls: "xls",
40
- ppt: "ppt",
41
- epub: "epub",
42
- csv: "csv",
43
- svg: "svg",
44
- pdf: "pdf"
45
- };
46
- function inferFormatFromExtension(path) {
47
- const lastSegment = path.split(/[/\\]/).pop() ?? path;
48
- const dotIndex = lastSegment.lastIndexOf(".");
49
- if (dotIndex <= 0) return;
50
- const extension = lastSegment.slice(dotIndex + 1).toLowerCase();
51
- return EXTENSION_TO_FORMAT[extension];
52
- }
53
- /**
54
- * The hybrid input shape every MCP tool that accepts a document accepts: either a filesystem path (format inferred from its extension) or inline base64-encoded bytes (format required, since there is no filename to infer it from).
55
- */
56
- const DocumentInputSchema = z.union([z.object({ path: z.string().describe("Filesystem path to the document to read. The document format is inferred from the file extension.") }), z.object({
57
- bytesBase64: z.string().describe("Base64-encoded document bytes."),
58
- format: DocumentFormatSchema.describe("The document format of bytesBase64 -- required, since inline bytes carry no filename to infer it from.")
59
- })]);
60
- /**
61
- * Resolves the hybrid DocumentInput union to concrete bytes and a format: for the path shape, reads the file from disk and infers its format from the extension (throwing if the extension is unrecognised); for the bytesBase64 shape, decodes the inline payload and uses the caller-supplied format directly.
62
- */
63
- async function resolveDocumentInput(input, options) {
64
- if ("path" in input) {
65
- const format = inferFormatFromExtension(input.path);
66
- if (format === void 0) throw new Error(`Could not infer a document format from the file extension of "${input.path}". Recognised extensions: ${Object.keys(EXTENSION_TO_FORMAT).join(", ")}. Pass an explicit format via the bytesBase64 input shape instead.`);
67
- const buffer = await readFile(input.path, { signal: options?.signal });
68
- return {
69
- bytes: new Uint8Array(buffer),
70
- format
71
- };
72
- }
73
- return {
74
- bytes: base64ToBytes(input.bytesBase64),
75
- format: input.format
76
- };
77
- }
78
- //#endregion
79
- //#region src/tools/compute-formula.ts
80
- function toErrorResult$3(error) {
7
+ //#region src/register-operation.ts
8
+ function toErrorResult(error) {
81
9
  return {
82
10
  content: [{
83
11
  type: "text",
@@ -86,829 +14,83 @@ function toErrorResult$3(error) {
86
14
  isError: true
87
15
  };
88
16
  }
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;
17
+ /**
18
+ * Registers one document-operations DocumentOperation as an MCP tool: `title`/`description`/`inputSchema`/`outputSchema` come straight from the operation, and a successful `run()` result is wrapped in the identical `{ content: [...], structuredContent }` shape every tool in this package already returned before the operations moved out to document-operations.
19
+ *
20
+ * `mapError` lets a caller special-case one of the operation's own thrown error types into an enriched result (e.g. odb_render_report's OdbReportNotSpecifiedError, odm_to_pdf's OdmUnresolvedSectionError) -- return undefined to fall through to the default `toErrorResult` wrapping for every other error.
21
+ */
22
+ function registerOperation(server, operation, options) {
23
+ server.registerTool(operation.name, {
24
+ title: operation.title,
25
+ description: operation.description,
26
+ inputSchema: operation.inputSchema,
27
+ ...operation.outputSchema === void 0 ? {} : { outputSchema: operation.outputSchema }
28
+ }, async (args, ctx) => {
140
29
  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
- };
30
+ const result = await operation.run(args, { signal: ctx.mcpReq.signal });
159
31
  return {
160
32
  content: [{
161
33
  type: "text",
162
- text: JSON.stringify(structuredContent)
34
+ text: JSON.stringify(result)
163
35
  }],
164
- structuredContent
36
+ structuredContent: result
165
37
  };
166
38
  } catch (error) {
167
- return toErrorResult$3(error);
39
+ return options?.mapError?.(error) ?? toErrorResult(error);
168
40
  }
169
41
  });
170
42
  }
171
43
  //#endregion
172
- //#region src/io/document-output.ts
173
- /**
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.
175
- */
176
- const DocumentOutputSchema = z.object({ outputPath: z.string().optional().describe("Filesystem path to write the resulting document to. Omit to receive the document bytes inline, base64-encoded, in the tool result instead.") });
177
- /**
178
- * Resolves output bytes against the hybrid DocumentOutput shape: writes to `outputPath` and reports the path plus byte length when one is given, otherwise returns the bytes inline as base64 (flagged `large: true` above `LARGE_RESULT_THRESHOLD_BYTES`, never truncated or refused).
179
- */
180
- async function resolveDocumentOutput(bytes, output) {
181
- if (output.outputPath !== void 0) {
182
- await writeFile(output.outputPath, bytes);
183
- return {
184
- path: output.outputPath,
185
- byteLength: bytes.byteLength
186
- };
187
- }
188
- const bytesBase64 = bytesToBase64(bytes);
189
- if (bytes.byteLength > 5242880) return {
190
- bytesBase64,
191
- byteLength: bytes.byteLength,
192
- large: true
193
- };
194
- return {
195
- bytesBase64,
196
- byteLength: bytes.byteLength
197
- };
44
+ //#region src/tools/compute-formula.ts
45
+ function registerComputeFormulaTools(server) {
46
+ registerOperation(server, computeFormulaOperation);
198
47
  }
199
48
  //#endregion
200
49
  //#region src/tools/convert.ts
201
- const DiagnosticSchema = z.object({
202
- severity: z.enum(["info", "warning"]),
203
- code: z.string(),
204
- message: z.string(),
205
- pageIndex: z.number().optional()
206
- });
207
- const FontSubstitutionSchema$1 = z.object({
208
- requestedFamily: z.string(),
209
- requestedBold: z.boolean(),
210
- requestedItalic: z.boolean(),
211
- reason: z.enum(["missing-face", "vendored-substitute"]),
212
- resolvedFamily: z.string()
213
- });
214
- const ResolvedDocumentOutputSchema = z.union([z.object({
215
- path: z.string(),
216
- byteLength: z.number()
217
- }), z.object({
218
- bytesBase64: z.string(),
219
- byteLength: z.number(),
220
- large: z.literal(true).optional()
221
- })]);
222
- const FontInputSchema$1 = z.object({
223
- family: z.string().describe("The font family name this face provides."),
224
- bold: z.boolean().describe("Whether this face is the bold weight."),
225
- italic: z.boolean().describe("Whether this face is the italic slope."),
226
- bytesBase64: z.string().describe("Base64-encoded font program bytes (TrueType/OpenType/CFF) for this family/weight/style combination.")
227
- });
228
- const ConvertDocumentInputSchema = z.object({
229
- source: DocumentInputSchema.describe("The document to convert."),
230
- targetFormat: DocumentFormatSchema.describe("The format to convert the document to. Not every (source, targetFormat) pair is supported directly -- call list_document_conversions first to confirm this one is."),
231
- output: DocumentOutputSchema.optional().describe("Where to write the converted document. Omit entirely to receive the bytes inline, base64-encoded, instead."),
232
- fonts: z.array(FontInputSchema$1).optional().describe("Extra font faces to make available to the conversion, for a family the source document does not already embed. Only consulted by a conversion that runs a layout engine (a <format>-to-pdf conversion); every other conversion ignores this option entirely."),
233
- onSubstitutionDiagnostics: z.boolean().optional().describe("When true, additionally report each individual font-substitution event as structured fontSubstitutions (which family/weight/style was requested, what it resolved to instead, and why). Every substitution is always reported as a plain diagnostic in `diagnostics` regardless of this flag -- this only controls whether the fuller, structured event is also collected."),
234
- images: z.record(z.string(), z.string()).optional().describe("A map from a markdown image destination (the part in the parentheses of ![](..)) to its base64-encoded PNG/JPEG bytes, for resolving a markdown source's own non-data: images. Only consulted by a markdown-sourced conversion; every other conversion ignores it. A destination absent from the map degrades to alt text, matching documents.js's own MarkdownImageResolver port -- an MCP caller has no filesystem context to read a relative path from, so any image that is not a data: URI must be supplied here explicitly to be embedded.")
235
- });
236
- const ConvertDocumentOutputSchema = z.object({
237
- targetFormat: DocumentFormatSchema,
238
- output: ResolvedDocumentOutputSchema,
239
- diagnostics: z.array(DiagnosticSchema),
240
- fontSubstitutions: z.array(FontSubstitutionSchema$1).optional()
241
- });
242
- const ListDocumentConversionsOutputSchema = z.object({ conversions: z.array(z.object({
243
- source: DocumentFormatSchema,
244
- target: DocumentFormatSchema
245
- })) });
246
50
  function registerConvertTools(server) {
247
- const converter = createLocalDocumentConverter();
248
- server.registerTool("convert_document", {
249
- title: "Convert document",
250
- description: "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 supported directly (odf, for instance, only ever converts to pdf); call list_document_conversions first to see which pairs actually are.",
251
- inputSchema: ConvertDocumentInputSchema,
252
- outputSchema: ConvertDocumentOutputSchema
253
- }, async ({ source, targetFormat, output, fonts, onSubstitutionDiagnostics, images }, ctx) => {
254
- const { signal } = ctx.mcpReq;
255
- const { bytes, format } = await resolveDocumentInput(source, { signal });
256
- const fontSubstitutions = [];
257
- const result = await converter.convert({
258
- source: {
259
- format,
260
- bytes
261
- },
262
- targetFormat
263
- }, {
264
- signal,
265
- fonts: fonts?.map((font) => ({
266
- family: font.family,
267
- bold: font.bold,
268
- italic: font.italic,
269
- bytes: base64ToBytes(font.bytesBase64)
270
- })),
271
- onFontSubstitution: onSubstitutionDiagnostics === true ? (substitution) => fontSubstitutions.push(substitution) : void 0,
272
- images: images === void 0 ? void 0 : (destination) => {
273
- const base64 = images[destination];
274
- return base64 === void 0 ? void 0 : { bytes: base64ToBytes(base64) };
275
- }
276
- });
277
- const resolvedOutput = await resolveDocumentOutput(result.document.bytes, output ?? {});
278
- const structuredContent = {
279
- targetFormat: result.document.format,
280
- output: resolvedOutput,
281
- diagnostics: [...result.diagnostics],
282
- ...onSubstitutionDiagnostics === true ? { fontSubstitutions: [...fontSubstitutions] } : {}
283
- };
284
- return {
285
- content: [{
286
- type: "text",
287
- text: JSON.stringify(structuredContent)
288
- }],
289
- structuredContent
290
- };
291
- });
292
- server.registerTool("list_document_conversions", {
293
- title: "List document conversions",
294
- description: "Lists every (source, target) format pair convert_document actually supports, straight from documents.js's own DocumentConverter port -- the definitive source of truth for what convert_document will and will not accept as a (source, targetFormat) combination.",
295
- outputSchema: ListDocumentConversionsOutputSchema
296
- }, () => {
297
- const structuredContent = { conversions: converter.conversions.map((conversion) => ({
298
- source: conversion.source,
299
- target: conversion.target
300
- })) };
301
- return {
302
- content: [{
303
- type: "text",
304
- text: JSON.stringify(structuredContent)
305
- }],
306
- structuredContent
307
- };
308
- });
51
+ registerOperation(server, convertDocumentOperation);
52
+ registerOperation(server, listDocumentConversionsOperation);
309
53
  }
310
54
  //#endregion
311
55
  //#region src/tools/docx-extras.ts
312
56
  function registerDocxExtrasTools(server) {
313
- server.registerTool("docx_extras", {
314
- title: "Docx extras",
315
- description: "Reads a docx's own comments, footnotes, header/footer parts, and numbering definitions -- data documents.js's ContentDocument pivot cannot carry, so ordinary document-reading tools never see it. Returns the real DocxExtras object (comments/footnotes/headerFooterParts/sectionHeaderFooters/numbering) as structured data.",
316
- inputSchema: z.object({ source: DocumentInputSchema.describe("The docx document to read.") })
317
- }, async ({ source }) => {
318
- const { bytes } = await resolveDocumentInput(source);
319
- const pkg = decodePackage(bytes);
320
- const extras = readDocxExtras(pkg);
321
- return {
322
- content: [{
323
- type: "text",
324
- text: JSON.stringify(extras)
325
- }],
326
- structuredContent: extras
327
- };
328
- });
57
+ registerOperation(server, docxExtrasOperation);
329
58
  }
330
59
  //#endregion
331
60
  //#region src/tools/editor.ts
332
- function toErrorResult$2(error) {
333
- return {
334
- content: [{
335
- type: "text",
336
- text: error instanceof Error ? error.message : String(error)
337
- }],
338
- isError: true
339
- };
340
- }
341
- const WritableFormatSchema = z.enum([
342
- "docx",
343
- "pptx",
344
- "odt",
345
- "odp",
346
- "ods",
347
- "odg",
348
- "pdf",
349
- "markdown"
350
- ]);
351
- const RunSchema = z.object({
352
- text: z.string().optional().describe("The run's own text content."),
353
- bold: z.boolean().optional(),
354
- italic: z.boolean().optional(),
355
- strike: z.boolean().optional(),
356
- underline: z.boolean().optional().describe("docx/odt only."),
357
- fontFamily: z.string().optional().describe("docx/odt only."),
358
- sizePt: z.number().positive().optional().describe("docx/odt only."),
359
- colorHex: z.string().regex(/^#?[0-9a-fA-F]{6}$/).optional().describe("6-digit hex colour (e.g. 'ff0000' or '#ff0000'). docx/odt only."),
360
- hyperlink: z.string().optional().describe("markdown only."),
361
- code: z.boolean().optional().describe("markdown only -- renders the run as an inline code span.")
362
- });
363
- const ParagraphSchema = z.object({
364
- text: z.string().optional().describe("Plain paragraph text, as a single run. Omit and use `runs` instead for mixed formatting within one paragraph."),
365
- styleId: z.string().optional(),
366
- headingLevel: z.number().int().min(1).max(6).optional().describe("docx/odt only."),
367
- alignment: z.enum([
368
- "left",
369
- "center",
370
- "right",
371
- "justify"
372
- ]).optional().describe("docx/odt only."),
373
- runs: z.array(RunSchema).optional().describe("Runs to append to this paragraph, each with its own formatting. Combine with `text` for a paragraph that opens with plain text before its first formatted run, or omit `text` entirely for a paragraph built purely from runs.")
374
- });
375
- function unsupportedFieldNames(input, unsupported) {
376
- return unsupported.filter((key) => input[key] !== void 0);
377
- }
378
- function rejectUnsupportedFields(input, unsupported, formatLabel, context) {
379
- const present = unsupportedFieldNames(input, unsupported);
380
- if (present.length > 0) throw new Error(`${context} does not support ${present.join(", ")} for ${formatLabel} -- remove ${present.length === 1 ? "it" : "them"} or target docx/odt instead.`);
381
- }
382
- const DOCX_ODT_UNSUPPORTED_RUN_FIELDS = ["hyperlink", "code"];
383
- const MARKDOWN_UNSUPPORTED_RUN_FIELDS = [
384
- "underline",
385
- "fontFamily",
386
- "sizePt",
387
- "colorHex"
388
- ];
389
- const MARKDOWN_UNSUPPORTED_PARAGRAPH_FIELDS = ["headingLevel", "alignment"];
390
- function appendDocxOdtRun(paragraph, run) {
391
- rejectUnsupportedFields(run, DOCX_ODT_UNSUPPORTED_RUN_FIELDS, "docx/odt", "A run");
392
- paragraph.appendRun({
393
- text: run.text,
394
- bold: run.bold,
395
- italic: run.italic,
396
- underline: run.underline,
397
- strike: run.strike,
398
- fontFamily: run.fontFamily,
399
- sizePt: run.sizePt,
400
- color: run.colorHex === void 0 ? void 0 : rgbHexToColor(run.colorHex)
401
- });
402
- }
403
- function appendDocxOdtParagraphs(body, paragraphs) {
404
- for (const paragraph of paragraphs) {
405
- const built = body.appendParagraph({
406
- text: paragraph.text,
407
- styleId: paragraph.styleId,
408
- headingLevel: paragraph.headingLevel,
409
- alignment: paragraph.alignment
410
- });
411
- for (const run of paragraph.runs ?? []) appendDocxOdtRun(built, run);
412
- }
413
- }
414
- function appendMarkdownParagraphs(body, paragraphs) {
415
- for (const paragraph of paragraphs) {
416
- rejectUnsupportedFields(paragraph, MARKDOWN_UNSUPPORTED_PARAGRAPH_FIELDS, "markdown", "A paragraph");
417
- const built = body.appendParagraph({
418
- text: paragraph.text,
419
- styleId: paragraph.styleId
420
- });
421
- for (const run of paragraph.runs ?? []) {
422
- rejectUnsupportedFields(run, MARKDOWN_UNSUPPORTED_RUN_FIELDS, "markdown", "A run");
423
- built.appendRun({
424
- text: run.text,
425
- bold: run.bold,
426
- italic: run.italic,
427
- strike: run.strike,
428
- hyperlink: run.hyperlink,
429
- code: run.code
430
- });
431
- }
432
- }
433
- }
434
- function createDocumentBytes(format) {
435
- switch (format) {
436
- case "docx": return createDocx().toBytes();
437
- case "pptx": return createPptx().toBytes();
438
- case "odt": return createOdt().toBytes();
439
- case "odp": return createOdp().toBytes();
440
- case "ods": return createOds().toBytes();
441
- case "odg": return createOdg().toBytes();
442
- case "pdf": return createPdf().toBytes();
443
- case "markdown": return encodeMarkdownText(createMarkdownEditor().toMarkdownText());
444
- }
445
- }
446
61
  function registerEditorTools(server) {
447
- server.registerTool("document_create", {
448
- title: "Create a blank document",
449
- description: "Creates a fresh, blank document in the given format via documents.js's own live-view editors (createDocx/createOdt/createMarkdownEditor/...), the same construction document_append_paragraphs's own edit calls build on. Returns an empty document with no content -- follow with document_append_paragraphs (docx/odt/markdown) to add text.",
450
- inputSchema: z.object({
451
- format: WritableFormatSchema.describe("The format of document to create."),
452
- output: DocumentOutputSchema.optional().describe("Where to write the created document. Omit entirely to receive the bytes inline, base64-encoded.")
453
- })
454
- }, async ({ format, output }) => {
455
- try {
456
- const resolvedOutput = await resolveDocumentOutput(createDocumentBytes(format), output ?? {});
457
- return {
458
- content: [{
459
- type: "text",
460
- text: JSON.stringify(resolvedOutput)
461
- }],
462
- structuredContent: resolvedOutput
463
- };
464
- } catch (error) {
465
- return toErrorResult$2(error);
466
- }
467
- });
468
- server.registerTool("document_append_paragraphs", {
469
- title: "Append paragraphs to a wordprocessing document",
470
- description: "Appends one or more paragraphs -- each optionally built from several independently-formatted runs -- to the end of a docx, odt, or markdown document, through documents.js's own live-view editors (the same DocxBody.appendParagraph/OdtBody.appendParagraph/MarkdownBody.appendParagraph document-cli's own TUI uses). Does not convert format -- the source document's own format and targetFormat must match. docx and odt share an identical field set (underline, fontFamily, sizePt, colorHex, headingLevel, alignment all supported); markdown supports a different, smaller set (hyperlink, code) and rejects the docx/odt-only fields outright rather than silently dropping them. This covers wordprocessing paragraph/run editing only -- slides, sheets, drawings, tables, lists, and images are a separate, larger editing surface not exposed here yet.",
471
- inputSchema: z.object({
472
- source: DocumentInputSchema.describe("The document to append paragraphs to."),
473
- targetFormat: z.enum([
474
- "docx",
475
- "odt",
476
- "markdown"
477
- ]).describe("Must match the source document's own format. document_append_paragraphs never converts format."),
478
- paragraphs: z.array(ParagraphSchema).min(1).describe("The paragraphs to append, in order."),
479
- output: DocumentOutputSchema.optional().describe("Where to write the edited document. Omit entirely to receive the bytes inline, base64-encoded.")
480
- })
481
- }, async ({ source, targetFormat, paragraphs, output }) => {
482
- try {
483
- const { bytes, format } = await resolveDocumentInput(source);
484
- if (format !== targetFormat) throw new Error(`source document is "${format}", but targetFormat is "${targetFormat}" -- document_append_paragraphs never converts format, so the two must match.`);
485
- let resultBytes;
486
- if (targetFormat === "markdown") {
487
- const editor = openMarkdown(decodeMarkdownText(bytes));
488
- appendMarkdownParagraphs(editor.body, paragraphs);
489
- resultBytes = encodeMarkdownText(editor.toMarkdownText());
490
- } else {
491
- const editor = targetFormat === "docx" ? openDocx(bytes) : openOdt(bytes);
492
- appendDocxOdtParagraphs(editor.body, paragraphs);
493
- resultBytes = editor.toBytes();
494
- }
495
- const resolvedOutput = await resolveDocumentOutput(resultBytes, output ?? {});
496
- return {
497
- content: [{
498
- type: "text",
499
- text: JSON.stringify(resolvedOutput)
500
- }],
501
- structuredContent: resolvedOutput
502
- };
503
- } catch (error) {
504
- return toErrorResult$2(error);
505
- }
506
- });
62
+ registerOperation(server, documentCreateOperation);
63
+ registerOperation(server, documentAppendParagraphsOperation);
507
64
  }
508
65
  //#endregion
509
66
  //#region src/tools/fonts.ts
510
- const FontFileInputSchema = z.union([z.object({ path: z.string().describe("Filesystem path to the font file (.ttf/.otf) to read.") }), z.object({ bytesBase64: z.string().describe("Base64-encoded font file bytes.") })]);
511
- async function resolveFontFileInput(input) {
512
- if ("path" in input) {
513
- const buffer = await readFile(input.path);
514
- return {
515
- bytes: new Uint8Array(buffer),
516
- source: input.path
517
- };
518
- }
519
- return {
520
- bytes: base64ToBytes(input.bytesBase64),
521
- source: "inline font bytes"
522
- };
523
- }
524
- /** A tool result reporting a problem: `isError: true` with the message as the sole content block, never a thrown exception -- a thrown error from a tool callback surfaces as a JSON-RPC protocol error rather than a result the caller can inspect and recover from. Matches odb.ts's own errorResult/toErrorResult/jsonResult convention. */
525
- function errorResult$1(message) {
526
- return {
527
- content: [{
528
- type: "text",
529
- text: message
530
- }],
531
- isError: true
532
- };
533
- }
534
- /** errorResult's counterpart for a caught exception: UnsupportedFontSourceFormatError (extractSourceFontsForFormat rejecting a format with no source-embedded-font concept -- xlsx/pdf/markdown/odf) and FontFaceParseError (describeFontFace rejecting bytes that are not a recognised sfnt font, or a .ttc collection) both carry a self-contained, already-actionable message, so there is nothing to add beyond surfacing it verbatim. */
535
- function toErrorResult$1(error) {
536
- return errorResult$1(error instanceof Error ? error.message : String(error));
537
- }
538
- /** A successful tool result: the value serialised as the text content block, and returned verbatim as structuredContent for a caller that wants the parsed value directly rather than re-parsing the text block. */
539
- function jsonResult(value) {
540
- return {
541
- content: [{
542
- type: "text",
543
- text: JSON.stringify(value)
544
- }],
545
- structuredContent: value
546
- };
547
- }
548
67
  function registerFontTools(server) {
549
- server.registerTool("fonts", {
550
- title: "List document fonts",
551
- description: "Lists every source-embedded font face a docx/pptx/odt/odp/ods/odg document carries (family, weight/style, byte length).",
552
- inputSchema: z.object({ source: DocumentInputSchema.describe("The docx/pptx/odt/odp/ods/odg document to extract source-embedded font faces from.") })
553
- }, async ({ source }) => {
554
- try {
555
- const { bytes, format } = await resolveDocumentInput(source);
556
- return jsonResult({ faces: extractSourceFontsForFormat(format, bytes).map((face) => ({
557
- family: face.family,
558
- bold: face.bold,
559
- italic: face.italic,
560
- byteLength: face.bytes.length
561
- })) });
562
- } catch (error) {
563
- return toErrorResult$1(error);
564
- }
565
- });
566
- server.registerTool("describe_font_file", {
567
- title: "Describe font file",
568
- description: "Reads a standalone TrueType/OpenType font file (.ttf/.otf) and reports the family/bold/italic triple it declares about itself.",
569
- inputSchema: z.object({ source: FontFileInputSchema.describe("The standalone .ttf/.otf font file to inspect -- not a document.") })
570
- }, async ({ source }) => {
571
- try {
572
- const { bytes, source: label } = await resolveFontFileInput(source);
573
- const face = describeFontFace(bytes, label);
574
- return jsonResult({
575
- family: face.family,
576
- bold: face.bold,
577
- italic: face.italic
578
- });
579
- } catch (error) {
580
- return toErrorResult$1(error);
581
- }
582
- });
68
+ registerOperation(server, fontsOperation);
69
+ registerOperation(server, describeFontFileOperation);
583
70
  }
584
71
  //#endregion
585
72
  //#region src/tools/from-package.ts
586
- async function readSourceBytes(source) {
587
- if ("path" in source) {
588
- const buffer = await readFile(source.path);
589
- return new Uint8Array(buffer);
590
- }
591
- return base64ToBytes(source.bytesBase64);
592
- }
593
- function errorResult(message) {
594
- return {
595
- content: [{
596
- type: "text",
597
- text: message
598
- }],
599
- isError: true
600
- };
601
- }
602
- function isNamedError(error, name) {
603
- return error instanceof Error && error.name === name;
604
- }
605
73
  function registerFromPackageTools(server) {
606
- server.registerTool("from_package", {
607
- title: "Build document from package",
608
- description: "Rebuilds real document bytes in a target format from a DocumentTree previously serialised to JSON (e.g. by a caller's own --dump-package-equivalent step) -- the read side of the DocumentTree round trip a conversion's onDocument callback produces.",
609
- inputSchema: z.object({
610
- source: DocumentInputSchema.describe("The DocumentTree JSON to read. 'path' points at a JSON file on disk -- its extension is never used to infer a document format, since the file holds a DocumentTree, not a document. 'bytesBase64' carries the JSON inline; its 'format' field is required by the shared hybrid input shape but unused by this tool."),
611
- targetFormat: DocumentFormatSchema.describe("The document format to build from the DocumentTree."),
612
- output: DocumentOutputSchema.optional().describe("Where to write the resulting document. Omit entirely (or omit outputPath within it) to receive the bytes inline instead.")
613
- })
614
- }, async ({ source, targetFormat, output }) => {
615
- let parsed;
616
- try {
617
- const bytes = await readSourceBytes(source);
618
- const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
619
- try {
620
- parsed = JSON.parse(text);
621
- } catch (error) {
622
- return errorResult(`'source' is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
623
- }
624
- const result = documentFromJson(parsed);
625
- if (result.kind !== "DocumentTree") return errorResult(`'source' is a ${result.kind}, not a DocumentTree -- only a file carrying a real DocumentTree (e.g. written by a caller's own --dump-package-equivalent step) can be read back by this tool`);
626
- const resolvedOutput = await resolveDocumentOutput(buildDocumentBytes(result.value, targetFormat), output ?? {});
627
- return {
628
- content: [{
629
- type: "text",
630
- text: JSON.stringify(resolvedOutput)
631
- }],
632
- structuredContent: resolvedOutput
633
- };
634
- } catch (error) {
635
- if (error instanceof UnrecognizedDocumentSchemaError) return errorResult("'source' has no recognised $schema -- only a file carrying a real DocumentTree (e.g. written by a caller's own --dump-package-equivalent step) can be read back by this tool");
636
- if (isNamedError(error, "SchemaVersionMismatchError")) return errorResult(error.message);
637
- if (isNamedError(error, "LayoutSchemaDemotedError")) return errorResult(error.message);
638
- if (isNamedError(error, "DocumentPackageRenamedError")) return errorResult(error.message);
639
- if (error instanceof z.ZodError) return errorResult(`'source' failed ${documentSchemaKindOf(parsed) ?? "document schema"} validation: ${error.message}`);
640
- return errorResult(error instanceof Error ? error.message : String(error));
641
- }
642
- });
74
+ registerOperation(server, fromPackageOperation);
643
75
  }
644
76
  //#endregion
645
77
  //#region src/tools/metadata.ts
646
- function toErrorResult(error) {
647
- return {
648
- content: [{
649
- type: "text",
650
- text: error instanceof Error ? error.message : String(error)
651
- }],
652
- isError: true
653
- };
654
- }
655
78
  function registerMetadataTools(server) {
656
- server.registerTool("metadata_read", {
657
- title: "Read document metadata",
658
- description: "Reads a document's own title/author/subject/keywords/creator/producer/created-and-modified-timestamp metadata. Works across every supported format, including xlsx (read via a throwaway xlsx-to-pdf preview, since documents.js has no dedicated xlsx metadata reader of its own) and odf (a standalone formula document).",
659
- inputSchema: z.object({ source: DocumentInputSchema.describe("The document to read metadata from.") })
660
- }, async ({ source }) => {
661
- try {
662
- const { bytes, format } = await resolveDocumentInput(source);
663
- const metadata = readDocumentMetadata(format, bytes);
664
- return {
665
- content: [{
666
- type: "text",
667
- text: JSON.stringify(metadata)
668
- }],
669
- structuredContent: metadata
670
- };
671
- } catch (error) {
672
- return toErrorResult(error);
673
- }
674
- });
675
- server.registerTool("metadata_write", {
676
- title: "Write document metadata",
677
- description: "Patches a document's own title/author/subject/keywords, leaving every other field and every other flag as-is. Does not convert format -- the source document's own format and targetFormat must match (or both be 'pdf'); odf (a standalone formula document) is rejected outright as either a source or a target, since it has no write path back out at all. Convert the document to a different format first (e.g. with a documents.js conversion tool) if metadata needs to be set on the result of a format change.",
678
- inputSchema: z.object({
679
- source: DocumentInputSchema.describe("The document to patch metadata on."),
680
- targetFormat: DocumentFormatSchema.describe("The format to write the patched document back out as -- must match the source document's own format (or both be 'pdf'). metadata_write never converts format."),
681
- output: DocumentOutputSchema.optional().describe("Where to write the patched document. Omit entirely to receive the bytes inline, base64-encoded."),
682
- setTitle: z.string().optional().describe("Set the title field. Omit to leave it exactly as the source document already has it."),
683
- setAuthor: z.string().optional().describe("Set the author field. Omit to leave it exactly as the source document already has it."),
684
- setSubject: z.string().optional().describe("Set the subject field. Omit to leave it exactly as the source document already has it."),
685
- setKeywords: z.array(z.string()).optional().describe("Set the keywords field. Omit to leave it exactly as the source document already has it.")
686
- })
687
- }, async ({ source, targetFormat, output, setTitle, setAuthor, setSubject, setKeywords }) => {
688
- try {
689
- const { bytes, format } = await resolveDocumentInput(source);
690
- const resolvedOutput = await resolveDocumentOutput(setDocumentMetadata(format, targetFormat, bytes, {
691
- title: setTitle,
692
- author: setAuthor,
693
- subject: setSubject,
694
- keywords: setKeywords
695
- }), output ?? {});
696
- return {
697
- content: [{
698
- type: "text",
699
- text: JSON.stringify(resolvedOutput)
700
- }],
701
- structuredContent: resolvedOutput
702
- };
703
- } catch (error) {
704
- return toErrorResult(error);
705
- }
706
- });
79
+ registerOperation(server, metadataReadOperation);
80
+ registerOperation(server, metadataWriteOperation);
707
81
  }
708
82
  //#endregion
709
83
  //#region src/tools/odb.ts
710
- const ODB_SOURCE_DESCRIPTION = ".odb database to read. 'path' points at the .odb file on disk -- its extension is never used to infer a document format, since documents.js deliberately excludes 'odb' from DocumentFormat (an embedded database front end has no single natural target format -- tables, saved queries, and reports are three unrelated output shapes -- see that package's own README). 'bytesBase64' carries the .odb bytes inline; its 'format' field is required by the shared hybrid input shape but unused by every odb tool.";
711
- async function resolveOdbBytes$1(source) {
712
- if ("path" in source) {
713
- const buffer = await readFile(source.path);
714
- return new Uint8Array(buffer);
715
- }
716
- return base64ToBytes(source.bytesBase64);
717
- }
718
- /** Resolves an .odb DocumentInput straight through to a decoded Package (via documents.js's own decodeOdbPackage) -- the shape every read (as opposed to export) odb tool needs. */
719
- async function resolveOdbPackage(source) {
720
- return decodeOdbPackage(await resolveOdbBytes$1(source));
721
- }
722
- /** Resolves a saved query's own SQL text by name against the .odb's own db:queries (read via readOdbInventory) -- odb_query's own equivalent of document-cli's resolveQuerySql, for the case where the caller named a saved query rather than supplying SQL directly. Throws, naming every available query, when the name doesn't resolve -- see this module's own top-of-file note on why a thrown Error is the right shape here. */
723
- function resolveSavedQuerySql(pkg, name) {
724
- const inventory = readOdbInventory(pkg);
725
- const saved = inventory.queries.find((candidate) => candidate.name === name);
726
- if (saved === void 0) {
727
- const available = inventory.queries.map((candidate) => candidate.name);
728
- throw new Error(`This .odb declares no saved query named "${name}".${available.length === 0 ? "" : ` Available: ${available.join(", ")}.`}`);
729
- }
730
- return saved.command;
731
- }
732
- /** Classifies odb_query's own sql/query pair before any package is read: exactly one of the two must be given. Throws for either "both given" or "neither given". Kept as a pure, synchronous check (no Package involved) so both variables narrow cleanly through ordinary control flow, rather than needing a cross-variable inference TypeScript can't derive from two separate `!== undefined` checks. */
733
- function classifyQueryInput(sql, query) {
734
- if (sql !== void 0 && query !== void 0) throw new Error("Provide \"sql\" or \"query\", not both.");
735
- if (sql !== void 0) return {
736
- kind: "literal",
737
- sql
738
- };
739
- if (query !== void 0) return {
740
- kind: "saved",
741
- name: query
742
- };
743
- throw new Error("Provide either \"sql\" (a literal SELECT statement) or \"query\" (the name of one of this .odb's own saved queries).");
744
- }
745
84
  function registerOdbTools(server) {
746
- server.registerTool("odb_tables", {
747
- title: "List .odb tables",
748
- description: "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 script formats, Firebird gbak backups).",
749
- inputSchema: z.object({ source: DocumentInputSchema.describe(ODB_SOURCE_DESCRIPTION) })
750
- }, async ({ source }) => {
751
- const pkg = await resolveOdbPackage(source);
752
- const tables = readOdbTables(pkg);
753
- return {
754
- content: [{
755
- type: "text",
756
- text: JSON.stringify(tables)
757
- }],
758
- structuredContent: tables
759
- };
760
- });
761
- server.registerTool("odb_forms", {
762
- title: "List .odb forms",
763
- description: "Lists every form an .odb database declares, with each form's own data source and field-bound controls.",
764
- inputSchema: z.object({ source: DocumentInputSchema.describe(ODB_SOURCE_DESCRIPTION) })
765
- }, async ({ source }) => {
766
- const pkg = await resolveOdbPackage(source);
767
- const forms = readOdbForms(pkg);
768
- return {
769
- content: [{
770
- type: "text",
771
- text: JSON.stringify(forms)
772
- }],
773
- structuredContent: forms
774
- };
775
- });
776
- server.registerTool("odb_reports", {
777
- title: "List .odb reports",
778
- description: "Lists every report an .odb database declares, with each report's own data-source command, band/group structure, and rpt: formula expressions.",
779
- inputSchema: z.object({ source: DocumentInputSchema.describe(ODB_SOURCE_DESCRIPTION) })
780
- }, async ({ source }) => {
781
- const pkg = await resolveOdbPackage(source);
782
- const reports = readOdbReports(pkg);
783
- return {
784
- content: [{
785
- type: "text",
786
- text: JSON.stringify(reports)
787
- }],
788
- structuredContent: reports
789
- };
790
- });
791
- server.registerTool("odb_query", {
792
- title: "Query an .odb database",
793
- description: "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 own extracted tables, given directly as SQL or by naming one of the database's saved queries. No database engine is involved -- the query runs in memory over the same tables odb_tables would return, against a closed grammar (SELECT/FROM/JOIN [INNER|LEFT [OUTER]|RIGHT [OUTER]|FULL [OUTER]|CROSS|NATURAL] ... [ON|USING]/WHERE/GROUP BY/ORDER BY, with an optional [AS] alias on any table, WHERE and ON also accepting [NOT] IN (SELECT ...) and [NOT] EXISTS (SELECT ...) -- no column aliases); an unsupported construct is reported as a tool error naming it, never silently ignored.",
794
- inputSchema: z.object({
795
- source: DocumentInputSchema.describe(ODB_SOURCE_DESCRIPTION),
796
- sql: z.string().describe("A literal SELECT statement to run. Mutually exclusive with \"query\".").optional(),
797
- query: z.string().describe("The name of one of the .odb's own saved queries to run. Mutually exclusive with \"sql\".").optional()
798
- })
799
- }, async ({ source, sql, query }) => {
800
- const spec = classifyQueryInput(sql, query);
801
- const pkg = await resolveOdbPackage(source);
802
- const resolvedSql = spec.kind === "literal" ? spec.sql : resolveSavedQuerySql(pkg, spec.name);
803
- const result = evaluateSelect(parseSelect(resolvedSql), readOdbTables(pkg));
804
- return {
805
- content: [{
806
- type: "text",
807
- text: JSON.stringify(result)
808
- }],
809
- structuredContent: result
810
- };
811
- });
812
- server.registerTool("odb_to_csv", {
813
- title: "Export one .odb table to CSV",
814
- description: "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.",
815
- inputSchema: z.object({
816
- source: DocumentInputSchema.describe(ODB_SOURCE_DESCRIPTION),
817
- table: z.string().describe("The table to export -- required when the .odb declares more than one table.").optional(),
818
- output: DocumentOutputSchema.optional().describe("Where to write the resulting CSV. Omit entirely (or omit outputPath within it) to receive the bytes inline instead.")
819
- })
820
- }, async ({ source, table, output }, ctx) => {
821
- const bytes = await resolveOdbBytes$1(source);
822
- const resolvedOutput = await resolveDocumentOutput(odbToCsv(bytes, {
823
- signal: ctx.mcpReq.signal,
824
- table
825
- }), output ?? {});
826
- return {
827
- content: [{
828
- type: "text",
829
- text: JSON.stringify(resolvedOutput)
830
- }],
831
- structuredContent: resolvedOutput
832
- };
833
- });
834
- server.registerTool("odb_to_xlsx", {
835
- title: "Export .odb tables to xlsx",
836
- description: "Extracts every table an embedded .odb database declares into one xlsx workbook, one sheet per table.",
837
- inputSchema: z.object({
838
- source: DocumentInputSchema.describe(ODB_SOURCE_DESCRIPTION),
839
- output: DocumentOutputSchema.optional().describe("Where to write the resulting xlsx workbook. Omit entirely (or omit outputPath within it) to receive the bytes inline instead.")
840
- })
841
- }, async ({ source, output }, ctx) => {
842
- const bytes = await resolveOdbBytes$1(source);
843
- const resolvedOutput = await resolveDocumentOutput(odbToXlsx(bytes, { signal: ctx.mcpReq.signal }), output ?? {});
844
- return {
845
- content: [{
846
- type: "text",
847
- text: JSON.stringify(resolvedOutput)
848
- }],
849
- structuredContent: resolvedOutput
850
- };
851
- });
85
+ registerOperation(server, odbTablesOperation);
86
+ registerOperation(server, odbFormsOperation);
87
+ registerOperation(server, odbReportsOperation);
88
+ registerOperation(server, odbQueryOperation);
89
+ registerOperation(server, odbToCsvOperation);
90
+ registerOperation(server, odbToXlsxOperation);
852
91
  }
853
92
  //#endregion
854
93
  //#region src/tools/odb-render-report.ts
855
- async function resolveOdbBytes(source) {
856
- if ("path" in source) {
857
- const buffer = await readFile(source.path);
858
- return new Uint8Array(buffer);
859
- }
860
- return base64ToBytes(source.bytesBase64);
861
- }
862
- const FontInputSchema = z.object({
863
- family: z.string().describe("The font family name this face provides."),
864
- bold: z.boolean().describe("Whether this face is the bold weight."),
865
- italic: z.boolean().describe("Whether this face is the italic slope."),
866
- bytesBase64: z.string().describe("Base64-encoded font program bytes (TrueType/OpenType/CFF) for this family/weight/style combination.")
867
- });
868
- const OdbRenderReportTargetFormatSchema = z.enum([
869
- "docx",
870
- "odt",
871
- "pdf"
872
- ]);
873
- const OdbRenderReportInputSchema = z.object({
874
- source: DocumentInputSchema.describe(".odb database to render a report from. 'path' points at the .odb file on disk -- its extension is never used to infer a document format, since documents.js deliberately excludes 'odb' from DocumentFormat (an embedded database front end has no single natural target format -- tables, saved queries, and reports are three unrelated output shapes -- see that package's own README). 'bytesBase64' carries the .odb bytes inline; its 'format' field is required by the shared hybrid input shape but unused by this tool."),
875
- report: z.string().optional().describe("The name of the report to render. Required only when the .odb declares more than one report -- omitting it when exactly one is declared renders that one automatically."),
876
- targetFormat: OdbRenderReportTargetFormatSchema.describe("The format to render the report into."),
877
- output: DocumentOutputSchema.optional().describe("Where to write the rendered report. Omit entirely (or omit outputPath within it) to receive the bytes inline instead."),
878
- fonts: z.array(FontInputSchema).optional().describe("Extra font faces to make available, for a family the rendered report's own text otherwise falls back on. Only consulted when targetFormat is 'pdf' -- docx and odt output is genuine editable text with no font-embedding step of its own, so fonts is ignored for those two targets.")
879
- });
880
- const MathDiagnosticSchema = z.object({
881
- kind: z.enum(["unsupported-element", "approximated-element"]),
882
- detail: z.string(),
883
- sourcePath: z.string().optional()
884
- });
885
- const FontSubstitutionSchema = z.object({
886
- requestedFamily: z.string(),
887
- requestedBold: z.boolean(),
888
- requestedItalic: z.boolean(),
889
- reason: z.enum(["missing-face", "vendored-substitute"]),
890
- resolvedFamily: z.string()
891
- });
892
- const CharSubstitutionSchema = z.object({
893
- from: z.string(),
894
- to: z.string(),
895
- pageIndex: z.number()
896
- });
897
- const diagnosticsShape = {
898
- mathDiagnostics: z.array(MathDiagnosticSchema),
899
- fontSubstitutions: z.array(FontSubstitutionSchema),
900
- charSubstitutions: z.array(CharSubstitutionSchema)
901
- };
902
- const OdbRenderReportOutputSchema = z.union([z.object({
903
- path: z.string(),
904
- byteLength: z.number(),
905
- ...diagnosticsShape
906
- }), z.object({
907
- bytesBase64: z.string(),
908
- byteLength: z.number(),
909
- large: z.literal(true).optional(),
910
- ...diagnosticsShape
911
- })]);
912
94
  function odbReportNotSpecifiedResult(error) {
913
95
  return {
914
96
  isError: true,
@@ -920,229 +102,29 @@ function odbReportNotSpecifiedResult(error) {
920
102
  };
921
103
  }
922
104
  function registerOdbRenderReportTools(server) {
923
- server.registerTool("odb_render_report", {
924
- title: "Render .odb report",
925
- description: "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.",
926
- inputSchema: OdbRenderReportInputSchema,
927
- outputSchema: OdbRenderReportOutputSchema
928
- }, async ({ source, report, targetFormat, output, fonts }, ctx) => {
929
- const { signal } = ctx.mcpReq;
930
- const inputBytes = await resolveOdbBytes(source);
931
- const pkg = decodeOdbPackage(inputBytes);
932
- let content;
933
- try {
934
- content = readOdbReportContent(pkg, { report });
935
- } catch (error) {
936
- if (error instanceof OdbReportNotSpecifiedError) return odbReportNotSpecifiedResult(error);
937
- throw error;
938
- }
939
- const mathDiagnostics = [];
940
- const recordMathDiagnostic = (diagnostic, diagnosticContext) => {
941
- mathDiagnostics.push({
942
- kind: diagnostic.kind,
943
- detail: diagnostic.detail,
944
- sourcePath: diagnosticContext.sourcePath
945
- });
946
- };
947
- const fontSubstitutions = [];
948
- const charSubstitutions = [];
949
- let bytes;
950
- if (targetFormat === "docx") bytes = odbReportToDocx(content, {
951
- signal,
952
- onMathDiagnostic: recordMathDiagnostic
953
- });
954
- else if (targetFormat === "odt") bytes = odbReportToOdt(content, { signal });
955
- else bytes = odbReportToPdf(content, {
956
- signal,
957
- fonts: fonts?.map((font) => ({
958
- family: font.family,
959
- bold: font.bold,
960
- italic: font.italic,
961
- bytes: base64ToBytes(font.bytesBase64)
962
- })),
963
- onFontSubstitution: (substitution) => fontSubstitutions.push(substitution),
964
- onSubstitution: (substitution, substitutionContext) => {
965
- charSubstitutions.push({
966
- from: substitution.from,
967
- to: substitution.to,
968
- pageIndex: substitutionContext.pageIndex
969
- });
970
- },
971
- onMathDiagnostic: recordMathDiagnostic
972
- });
973
- const structuredContent = {
974
- ...await resolveDocumentOutput(bytes, output ?? {}),
975
- mathDiagnostics,
976
- fontSubstitutions,
977
- charSubstitutions
978
- };
979
- return {
980
- content: [{
981
- type: "text",
982
- text: JSON.stringify(structuredContent)
983
- }],
984
- structuredContent
985
- };
986
- });
105
+ registerOperation(server, odbRenderReportOperation, { mapError: (error) => error instanceof OdbReportNotSpecifiedError ? odbReportNotSpecifiedResult(error) : void 0 });
987
106
  }
988
107
  //#endregion
989
108
  //#region src/tools/odm.ts
990
- const OdmMasterSourceSchema = z.union([z.object({ path: z.string().describe("Filesystem path to the .odm master document to convert.") }), z.object({ bytesBase64: z.string().describe("Base64-encoded .odm master document bytes.") })]);
991
- async function resolveOdmMasterBytes(source) {
992
- if ("path" in source) return new Uint8Array(await readFile(source.path));
993
- return base64ToBytes(source.bytesBase64);
994
- }
995
- const OdmChapterInputSchema = z.object({
996
- href: z.string().describe("The chapter's own text:section-source href as declared inside the .odm master document (e.g. '../chapter1.odt')."),
997
- source: DocumentInputSchema.describe("The chapter document's own bytes -- always read as odt, regardless of the format this hybrid input declares.")
998
- });
999
- const OdmToPdfInputSchema = z.object({
1000
- source: OdmMasterSourceSchema.describe("The .odm master document to convert."),
1001
- chapters: z.array(OdmChapterInputSchema).default([]).describe("Explicit href -> chapter document overrides. Checked before chaptersDir for a given href."),
1002
- chaptersDir: z.string().optional().describe("Directory to search for each unresolved chapter href, matched by the href's own basename. Checked after chapters."),
1003
- output: DocumentOutputSchema.default({}).describe("Where to write the resulting PDF. Omit to receive the bytes inline, base64-encoded.")
1004
- });
1005
- const OdmToPdfOutputSchema = z.union([z.object({
1006
- path: z.string(),
1007
- byteLength: z.number()
1008
- }), z.object({
1009
- bytesBase64: z.string(),
1010
- byteLength: z.number(),
1011
- large: z.literal(true).optional()
1012
- })]);
1013
109
  function registerOdmTools(server) {
1014
- server.registerTool("odm_to_pdf", {
1015
- title: "Convert ODM master document to PDF",
1016
- description: "Converts a .odm (ODF master document) to PDF. A .odm never carries its own chapters' content inline -- every text:section is a bare external reference to a standalone .odt file -- so each chapter the master document declares must resolve through `chapters` (an explicit href -> document override) and/or `chaptersDir` (a directory searched by the href's own basename), checked in that order. A chapter left unresolved by both fails the whole conversion, naming every unresolved href.",
1017
- inputSchema: OdmToPdfInputSchema,
1018
- outputSchema: OdmToPdfOutputSchema
1019
- }, async ({ source, chapters, chaptersDir, output }) => {
1020
- const masterBytes = await resolveOdmMasterBytes(source);
1021
- const overrides = /* @__PURE__ */ new Map();
1022
- for (const chapter of chapters) {
1023
- const { bytes: chapterBytes } = await resolveDocumentInput(chapter.source);
1024
- overrides.set(chapter.href, chapterBytes);
1025
- }
1026
- const resolveSubDocument = (href) => {
1027
- const overrideBytes = overrides.get(href);
1028
- if (overrideBytes !== void 0) return overrideBytes;
1029
- if (chaptersDir === void 0) return;
1030
- const candidate = join(chaptersDir, basename(href));
1031
- if (!existsSync(candidate)) return;
1032
- return new Uint8Array(readFileSync(candidate));
1033
- };
1034
- try {
1035
- const result = await resolveDocumentOutput(odmToPdf(masterBytes, { resolveSubDocument }), output);
1036
- return {
1037
- content: [{
1038
- type: "text",
1039
- text: JSON.stringify(result)
1040
- }],
1041
- structuredContent: result
1042
- };
1043
- } catch (error) {
1044
- if (error instanceof OdmUnresolvedSectionError) return {
1045
- isError: true,
1046
- content: [{
1047
- type: "text",
1048
- text: `${error.message}\nPass chaptersDir <dir> containing these files, or an explicit chapters override, for each href.`
1049
- }],
1050
- structuredContent: { hrefs: error.hrefs }
1051
- };
1052
- throw error;
1053
- }
1054
- });
110
+ registerOperation(server, odmToPdfOperation, { mapError: (error) => error instanceof OdmUnresolvedSectionError ? {
111
+ isError: true,
112
+ content: [{
113
+ type: "text",
114
+ text: `${error.message}\nPass chaptersDir <dir> containing these files, or an explicit chapters override, for each href.`
115
+ }],
116
+ structuredContent: { hrefs: error.hrefs }
117
+ } : void 0 });
1055
118
  }
1056
119
  //#endregion
1057
120
  //#region src/tools/outline.ts
1058
- function leafKind(leaf) {
1059
- if ("kind" in leaf) return leaf.kind;
1060
- if ("mathml" in leaf) return "formula";
1061
- return "embeddedObject";
1062
- }
1063
- function toOutlineJson(children) {
1064
- return children.map((child) => isOutlineNode(child) ? {
1065
- text: child.text,
1066
- level: child.level,
1067
- children: toOutlineJson(child.children)
1068
- } : {
1069
- kind: leafKind(child),
1070
- text: outlineLeafText(child)
1071
- });
1072
- }
1073
121
  function registerOutlineTools(server) {
1074
- server.registerTool("outline_document", {
1075
- title: "Outline document",
1076
- description: "Projects a document's table of contents as a structured outline: reads the source's own native DocumentTree directly (documents.js's readNativeDocumentTree -- no bridging conversion, no discarded output bytes) and runs document-outline.js's buildOutline over it. Groups carry { text, level, children } (a heading's, list item's, slide's, sheet's, or page's own label plus nested children); leaves carry { kind, text }.",
1077
- inputSchema: z.object({ source: DocumentInputSchema.describe("The document to outline.") })
1078
- }, async ({ source }, ctx) => {
1079
- const { signal } = ctx.mcpReq;
1080
- const { bytes, format } = await resolveDocumentInput(source, { signal });
1081
- const tree = readNativeDocumentTree(format, bytes, { signal });
1082
- const structuredContent = {
1083
- sourceFormat: format,
1084
- kind: tree.kind,
1085
- outline: toOutlineJson(buildOutline(tree))
1086
- };
1087
- return {
1088
- content: [{
1089
- type: "text",
1090
- text: JSON.stringify(structuredContent)
1091
- }],
1092
- structuredContent
1093
- };
1094
- });
122
+ registerOperation(server, outlineDocumentOperation);
1095
123
  }
1096
124
  //#endregion
1097
125
  //#region src/tools/pdf-inspect.ts
1098
- function buildItemKindHistogram(items) {
1099
- const histogram = /* @__PURE__ */ new Map();
1100
- for (const item of items) histogram.set(item.kind, (histogram.get(item.kind) ?? 0) + 1);
1101
- return histogram;
1102
- }
1103
- function countImagesByFormat(images) {
1104
- const counts = /* @__PURE__ */ new Map();
1105
- for (const asset of Object.values(images)) counts.set(asset.format, (counts.get(asset.format) ?? 0) + 1);
1106
- return counts;
1107
- }
1108
126
  function registerPdfInspectTools(server) {
1109
- server.registerTool("pdf_inspect", {
1110
- title: "Inspect PDF",
1111
- description: "Parses a PDF (documents.js's readPdf) and reports a summary: page count, each page's own size and item-kind histogram, document metadata, and embedded image formats. Pass full: true to return the entire parsed LayoutDocument instead of a summary.",
1112
- inputSchema: z.object({
1113
- source: DocumentInputSchema.describe("The PDF document to inspect."),
1114
- full: z.boolean().optional().describe("When true, return the entire parsed LayoutDocument instead of a summary. Defaults to false.")
1115
- })
1116
- }, async ({ source, full }, ctx) => {
1117
- const { signal } = ctx.mcpReq;
1118
- const { bytes, format } = await resolveDocumentInput(source, { signal });
1119
- if (format !== "pdf") throw new Error(`pdf_inspect requires a PDF document, received a "${format}" document instead.`);
1120
- const layout = readPdf(bytes, { signal });
1121
- if (full === true) return {
1122
- content: [{
1123
- type: "text",
1124
- text: JSON.stringify(layout)
1125
- }],
1126
- structuredContent: layout
1127
- };
1128
- const summary = {
1129
- pageCount: layout.pages.length,
1130
- pages: layout.pages.map((page) => ({
1131
- widthPt: page.widthPt,
1132
- heightPt: page.heightPt,
1133
- itemKinds: Object.fromEntries(buildItemKindHistogram(page.items))
1134
- })),
1135
- metadata: layout.metadata,
1136
- imagesByFormat: Object.fromEntries(countImagesByFormat(layout.images))
1137
- };
1138
- return {
1139
- content: [{
1140
- type: "text",
1141
- text: JSON.stringify(summary)
1142
- }],
1143
- structuredContent: summary
1144
- };
1145
- });
127
+ registerOperation(server, pdfInspectOperation);
1146
128
  }
1147
129
  //#endregion
1148
130
  //#region src/server.ts