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/README.md +8 -6
- package/dist/bin.js +51 -1069
- package/dist/index.cjs +50 -1068
- package/dist/index.js +51 -1069
- package/package.json +7 -8
package/dist/index.js
CHANGED
|
@@ -1,83 +1,11 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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.
|
|
5
|
+
var version = "4.10.0";
|
|
12
6
|
//#endregion
|
|
13
|
-
//#region src/
|
|
14
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
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(
|
|
34
|
+
text: JSON.stringify(result)
|
|
163
35
|
}],
|
|
164
|
-
structuredContent
|
|
36
|
+
structuredContent: result
|
|
165
37
|
};
|
|
166
38
|
} catch (error) {
|
|
167
|
-
return toErrorResult
|
|
39
|
+
return options?.mapError?.(error) ?? toErrorResult(error);
|
|
168
40
|
}
|
|
169
41
|
});
|
|
170
42
|
}
|
|
171
43
|
//#endregion
|
|
172
|
-
//#region src/
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
248
|
-
server
|
|
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
|
|
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
|
|
448
|
-
|
|
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
|
|
550
|
-
|
|
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
|
|
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
|
|
657
|
-
|
|
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
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
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
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
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
|
|
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
|