@bendyline/docblocks-cli 1.1.1 → 2.0.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 +60 -76
- package/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +61 -45
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/convert.d.ts +12 -2
- package/dist/commands/convert.d.ts.map +1 -1
- package/dist/commands/convert.js +272 -101
- package/dist/commands/convert.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +90 -4
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/parse.js +1 -1
- package/dist/commands/parse.js.map +1 -1
- package/dist/commands/serve.d.ts +33 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +340 -3
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/themes.js +1 -1
- package/dist/commands/themes.js.map +1 -1
- package/dist/commands/transforms.js +1 -1
- package/dist/commands/transforms.js.map +1 -1
- package/dist/commands/video.d.ts +8 -1
- package/dist/commands/video.d.ts.map +1 -1
- package/dist/commands/video.js +50 -30
- package/dist/commands/video.js.map +1 -1
- package/dist/contained-file.d.ts +8 -0
- package/dist/contained-file.d.ts.map +1 -0
- package/dist/contained-file.js +77 -0
- package/dist/contained-file.js.map +1 -0
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/agentic-tools.d.ts +16 -0
- package/dist/mcp/agentic-tools.d.ts.map +1 -0
- package/dist/mcp/agentic-tools.js +967 -0
- package/dist/mcp/agentic-tools.js.map +1 -0
- package/dist/mcp/artifact-store.d.ts +98 -0
- package/dist/mcp/artifact-store.d.ts.map +1 -0
- package/dist/mcp/artifact-store.js +542 -0
- package/dist/mcp/artifact-store.js.map +1 -0
- package/dist/mcp/authority.d.ts +53 -0
- package/dist/mcp/authority.d.ts.map +1 -0
- package/dist/mcp/authority.js +433 -0
- package/dist/mcp/authority.js.map +1 -0
- package/dist/mcp/conversion-service.d.ts +111 -0
- package/dist/mcp/conversion-service.d.ts.map +1 -0
- package/dist/mcp/conversion-service.js +512 -0
- package/dist/mcp/conversion-service.js.map +1 -0
- package/dist/mcp/discovery-tools.d.ts +4 -0
- package/dist/mcp/discovery-tools.d.ts.map +1 -0
- package/dist/mcp/discovery-tools.js +114 -0
- package/dist/mcp/discovery-tools.js.map +1 -0
- package/dist/mcp/document-service.d.ts +47 -0
- package/dist/mcp/document-service.d.ts.map +1 -0
- package/dist/mcp/document-service.js +424 -0
- package/dist/mcp/document-service.js.map +1 -0
- package/dist/mcp/error-result.d.ts +17 -0
- package/dist/mcp/error-result.d.ts.map +1 -0
- package/dist/mcp/error-result.js +82 -0
- package/dist/mcp/error-result.js.map +1 -0
- package/dist/mcp/intelligence.d.ts +15 -0
- package/dist/mcp/intelligence.d.ts.map +1 -0
- package/dist/mcp/intelligence.js +855 -0
- package/dist/mcp/intelligence.js.map +1 -0
- package/dist/mcp/output-bounds.d.ts +13 -0
- package/dist/mcp/output-bounds.d.ts.map +1 -0
- package/dist/mcp/output-bounds.js +61 -0
- package/dist/mcp/output-bounds.js.map +1 -0
- package/dist/mcp/preview-service.d.ts +54 -0
- package/dist/mcp/preview-service.d.ts.map +1 -0
- package/dist/mcp/preview-service.js +454 -0
- package/dist/mcp/preview-service.js.map +1 -0
- package/dist/mcp/progress.d.ts +18 -0
- package/dist/mcp/progress.d.ts.map +1 -0
- package/dist/mcp/progress.js +28 -0
- package/dist/mcp/progress.js.map +1 -0
- package/dist/mcp/prompts.d.ts +4 -0
- package/dist/mcp/prompts.d.ts.map +1 -0
- package/dist/mcp/prompts.js +124 -0
- package/dist/mcp/prompts.js.map +1 -0
- package/dist/mcp/rendered-conversion.d.ts +34 -0
- package/dist/mcp/rendered-conversion.d.ts.map +1 -0
- package/dist/mcp/rendered-conversion.js +364 -0
- package/dist/mcp/rendered-conversion.js.map +1 -0
- package/dist/mcp/server.d.ts +67 -10
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +228 -496
- package/dist/mcp/server.js.map +1 -1
- package/dist/preview-policy.d.ts +8 -0
- package/dist/preview-policy.d.ts.map +1 -0
- package/dist/preview-policy.js +85 -0
- package/dist/preview-policy.js.map +1 -0
- package/dist/render-html.d.ts +17 -0
- package/dist/render-html.d.ts.map +1 -0
- package/dist/render-html.js +85 -0
- package/dist/render-html.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +104 -0
- package/dist/version.js.map +1 -0
- package/package.json +11 -11
- package/dist/converters/docx-to-md.d.ts +0 -10
- package/dist/converters/docx-to-md.d.ts.map +0 -1
- package/dist/converters/docx-to-md.js +0 -17
- package/dist/converters/docx-to-md.js.map +0 -1
- package/dist/converters/pdf-to-md.d.ts +0 -14
- package/dist/converters/pdf-to-md.d.ts.map +0 -1
- package/dist/converters/pdf-to-md.js +0 -51
- package/dist/converters/pdf-to-md.js.map +0 -1
- package/dist/converters/pptx-to-md.d.ts +0 -13
- package/dist/converters/pptx-to-md.d.ts.map +0 -1
- package/dist/converters/pptx-to-md.js +0 -67
- package/dist/converters/pptx-to-md.js.map +0 -1
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { MCP_WIRE_LIMITS, parseComparisonResult, parseConversionResult, parseDocumentSource, parseInspectionResult, parseMaterializationOptions, parsePreviewResult, parseValidationResult, } from '@bendyline/docblocks/mcp';
|
|
4
|
+
import { MCP_FORMAT_CAPABILITIES, MCP_FORMAT_FIDELITIES, convertPreparedDocument, } from './conversion-service.js';
|
|
5
|
+
import { DocumentService, throwIfAborted, warningDiagnostic } from './document-service.js';
|
|
6
|
+
import { comparePreparedDocuments, inspectPreparedDocument, validatePreparedDocument, } from './intelligence.js';
|
|
7
|
+
import { artifactUriSchema, bundleDocumentSourceSchema, DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS, documentSourceSchema, formatInputSchema, materializationOptionsSchema, } from '@bendyline/docblocks/mcp/zod';
|
|
8
|
+
import { previewPreparedDocument, previewVideoSource, } from './preview-service.js';
|
|
9
|
+
import { errorResult, successResult } from './error-result.js';
|
|
10
|
+
import { reportMcpProgress } from './progress.js';
|
|
11
|
+
import { MCP_OUTPUT_PATTERNS, boundNullableWireText, boundWireText, boundWireTextArray, boundWireWarnings, requireWireIdentifier, toWireIdentifier, } from './output-bounds.js';
|
|
12
|
+
const READ_ONLY = {
|
|
13
|
+
readOnlyHint: true,
|
|
14
|
+
destructiveHint: false,
|
|
15
|
+
idempotentHint: true,
|
|
16
|
+
openWorldHint: false,
|
|
17
|
+
};
|
|
18
|
+
const ARTIFACT_CREATING = {
|
|
19
|
+
readOnlyHint: false,
|
|
20
|
+
destructiveHint: false,
|
|
21
|
+
idempotentHint: false,
|
|
22
|
+
openWorldHint: false,
|
|
23
|
+
};
|
|
24
|
+
const MATERIALIZING = {
|
|
25
|
+
readOnlyHint: false,
|
|
26
|
+
destructiveHint: true,
|
|
27
|
+
idempotentHint: false,
|
|
28
|
+
openWorldHint: false,
|
|
29
|
+
};
|
|
30
|
+
const identifierSchema = z
|
|
31
|
+
.string()
|
|
32
|
+
.min(1)
|
|
33
|
+
.max(MCP_WIRE_LIMITS.identifierCharacters)
|
|
34
|
+
.regex(MCP_OUTPUT_PATTERNS.identifier);
|
|
35
|
+
const fidelitySchemas = {
|
|
36
|
+
editable: z.enum(MCP_FORMAT_FIDELITIES.docx),
|
|
37
|
+
pdf: z.enum(MCP_FORMAT_FIDELITIES.pdf),
|
|
38
|
+
pptx: z.enum(MCP_FORMAT_FIDELITIES.pptx),
|
|
39
|
+
};
|
|
40
|
+
const metadataFields = {
|
|
41
|
+
title: z.string().max(4000).optional(),
|
|
42
|
+
author: z.string().max(4000).optional(),
|
|
43
|
+
description: z.string().max(8000).optional(),
|
|
44
|
+
};
|
|
45
|
+
const simpleTarget = (format, fidelity) => z.object({ format: z.literal(format), fidelity: fidelity.optional() }).strict();
|
|
46
|
+
const conversionTargetSchema = z.discriminatedUnion('format', [
|
|
47
|
+
simpleTarget('md', z.enum(MCP_FORMAT_FIDELITIES.md)),
|
|
48
|
+
simpleTarget('dbk', z.enum(MCP_FORMAT_FIDELITIES.dbk)),
|
|
49
|
+
z
|
|
50
|
+
.object({
|
|
51
|
+
format: z.literal('docx'),
|
|
52
|
+
fidelity: fidelitySchemas.editable.optional(),
|
|
53
|
+
...metadataFields,
|
|
54
|
+
defaultFont: z.string().max(256).optional(),
|
|
55
|
+
defaultFontSize: z.number().min(6).max(96).optional(),
|
|
56
|
+
})
|
|
57
|
+
.strict(),
|
|
58
|
+
z
|
|
59
|
+
.object({
|
|
60
|
+
format: z.literal('pdf'),
|
|
61
|
+
fidelity: fidelitySchemas.pdf.optional(),
|
|
62
|
+
title: metadataFields.title,
|
|
63
|
+
author: metadataFields.author,
|
|
64
|
+
pageSize: z.enum(['letter', 'a4']).optional(),
|
|
65
|
+
margin: z.number().min(0).max(288).optional(),
|
|
66
|
+
defaultFontSize: z.number().min(6).max(96).optional(),
|
|
67
|
+
width: z.number().int().min(160).max(1920).optional(),
|
|
68
|
+
height: z.number().int().min(90).max(1920).optional(),
|
|
69
|
+
})
|
|
70
|
+
.strict(),
|
|
71
|
+
z
|
|
72
|
+
.object({
|
|
73
|
+
format: z.literal('pptx'),
|
|
74
|
+
fidelity: fidelitySchemas.pptx.optional(),
|
|
75
|
+
...metadataFields,
|
|
76
|
+
slideBreak: z.enum(['h1', 'h2', 'heading']).optional(),
|
|
77
|
+
defaultFont: z.string().max(256).optional(),
|
|
78
|
+
defaultFontSize: z.number().min(6).max(96).optional(),
|
|
79
|
+
width: z.number().int().min(160).max(1920).optional(),
|
|
80
|
+
height: z.number().int().min(90).max(1920).optional(),
|
|
81
|
+
})
|
|
82
|
+
.strict(),
|
|
83
|
+
z
|
|
84
|
+
.object({
|
|
85
|
+
format: z.literal('xlsx'),
|
|
86
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.xlsx).optional(),
|
|
87
|
+
title: metadataFields.title,
|
|
88
|
+
author: metadataFields.author,
|
|
89
|
+
sheetNamePrefix: z.string().max(31).optional(),
|
|
90
|
+
})
|
|
91
|
+
.strict(),
|
|
92
|
+
z
|
|
93
|
+
.object({
|
|
94
|
+
format: z.literal('csv'),
|
|
95
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.csv).optional(),
|
|
96
|
+
delimiter: z.string().min(1).max(4).optional(),
|
|
97
|
+
tableIndex: z.number().int().nonnegative().max(10000).optional(),
|
|
98
|
+
})
|
|
99
|
+
.strict(),
|
|
100
|
+
z
|
|
101
|
+
.object({
|
|
102
|
+
format: z.literal('html'),
|
|
103
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.html).optional(),
|
|
104
|
+
mode: z.enum(['slideshow', 'static']).optional(),
|
|
105
|
+
autoPlay: z.boolean().optional(),
|
|
106
|
+
title: metadataFields.title,
|
|
107
|
+
})
|
|
108
|
+
.strict(),
|
|
109
|
+
z
|
|
110
|
+
.object({
|
|
111
|
+
format: z.literal('htmlzip'),
|
|
112
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.htmlzip).optional(),
|
|
113
|
+
mode: z.enum(['slideshow', 'static']).optional(),
|
|
114
|
+
autoPlay: z.boolean().optional(),
|
|
115
|
+
title: metadataFields.title,
|
|
116
|
+
})
|
|
117
|
+
.strict(),
|
|
118
|
+
z
|
|
119
|
+
.object({
|
|
120
|
+
format: z.literal('epub'),
|
|
121
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.epub).optional(),
|
|
122
|
+
...metadataFields,
|
|
123
|
+
language: z.string().max(64).optional(),
|
|
124
|
+
publisher: z.string().max(4000).optional(),
|
|
125
|
+
})
|
|
126
|
+
.strict(),
|
|
127
|
+
z
|
|
128
|
+
.object({
|
|
129
|
+
format: z.literal('mp4'),
|
|
130
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.mp4).optional(),
|
|
131
|
+
fps: z.number().int().min(1).max(60).optional(),
|
|
132
|
+
quality: z.enum(['draft', 'normal', 'high']).optional(),
|
|
133
|
+
orientation: z.enum(['landscape', 'portrait']).optional(),
|
|
134
|
+
width: z.number().int().min(16).max(3840).optional(),
|
|
135
|
+
height: z.number().int().min(16).max(3840).optional(),
|
|
136
|
+
captionStyle: z.enum(['standard', 'social']).optional(),
|
|
137
|
+
coverPreRoll: z.number().min(0).max(60).optional(),
|
|
138
|
+
animationsEnabled: z.boolean().optional(),
|
|
139
|
+
})
|
|
140
|
+
.strict(),
|
|
141
|
+
z
|
|
142
|
+
.object({
|
|
143
|
+
format: z.literal('gif'),
|
|
144
|
+
fidelity: z.enum(MCP_FORMAT_FIDELITIES.gif).optional(),
|
|
145
|
+
fps: z.number().int().min(1).max(30).optional(),
|
|
146
|
+
orientation: z.enum(['landscape', 'portrait']).optional(),
|
|
147
|
+
width: z.number().int().min(16).max(1920).optional(),
|
|
148
|
+
height: z.number().int().min(16).max(1920).optional(),
|
|
149
|
+
captionStyle: z.enum(['standard', 'social']).optional(),
|
|
150
|
+
coverPreRoll: z.number().min(0).max(60).optional(),
|
|
151
|
+
animationsEnabled: z.boolean().optional(),
|
|
152
|
+
loop: z.number().int().min(-1).max(65535).optional(),
|
|
153
|
+
maxColors: z.number().int().min(2).max(256).optional(),
|
|
154
|
+
dither: z
|
|
155
|
+
.enum(['none', 'bayer', 'heckbert', 'floyd_steinberg', 'sierra2', 'sierra2_4a'])
|
|
156
|
+
.optional(),
|
|
157
|
+
bayerScale: z.number().int().min(0).max(5).optional(),
|
|
158
|
+
})
|
|
159
|
+
.strict(),
|
|
160
|
+
]);
|
|
161
|
+
/** Exact target ids accepted by convert_document's runtime input schema. */
|
|
162
|
+
export const MCP_CONVERSION_TARGET_FORMATS = Object.freeze(conversionTargetSchema.options.map((schema) => schema.shape.format.value));
|
|
163
|
+
export function registerAgenticTools(server, context) {
|
|
164
|
+
registerArtifactResource(server, context.artifacts);
|
|
165
|
+
registerConversionReportResource(server, context.artifacts);
|
|
166
|
+
registerAuthoringGuideResource(server);
|
|
167
|
+
server.registerTool('list_roots', {
|
|
168
|
+
description: 'List opaque root aliases granted when this DocBlocks MCP server started. Root aliases improve path usability but never expand authority.',
|
|
169
|
+
inputSchema: z.object({}).strict(),
|
|
170
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.list_roots,
|
|
171
|
+
annotations: READ_ONLY,
|
|
172
|
+
}, async () => {
|
|
173
|
+
try {
|
|
174
|
+
const roots = (await context.authority).listRoots();
|
|
175
|
+
return textAndStructured({ roots });
|
|
176
|
+
}
|
|
177
|
+
catch (caught) {
|
|
178
|
+
return errorResult(caught, 'resolve');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
server.registerTool('get_conversion_report', {
|
|
182
|
+
description: 'Retrieve the original bounded diagnostics, applied options, fidelity, provenance, and source-asset manifest for a conversion-backed session artifact.',
|
|
183
|
+
inputSchema: z.object({ artifactUri: artifactUriSchema }).strict(),
|
|
184
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.get_conversion_report,
|
|
185
|
+
annotations: READ_ONLY,
|
|
186
|
+
}, async ({ artifactUri }) => {
|
|
187
|
+
try {
|
|
188
|
+
const report = await context.artifacts.getConversionReport(artifactUri);
|
|
189
|
+
requireWire(parseConversionResult(report), 'conversion report');
|
|
190
|
+
return textAndStructured(report);
|
|
191
|
+
}
|
|
192
|
+
catch (caught) {
|
|
193
|
+
return errorResult(caught, 'inspect');
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
server.registerTool('convert_document', {
|
|
197
|
+
description: 'Convert any linked-registry input into one or more immutable artifacts. Use save_artifact only when a durable filesystem file is required.',
|
|
198
|
+
inputSchema: z
|
|
199
|
+
.object({
|
|
200
|
+
source: documentSourceSchema,
|
|
201
|
+
targets: z.array(conversionTargetSchema).min(1).max(12),
|
|
202
|
+
themeId: identifierSchema.optional(),
|
|
203
|
+
transformId: identifierSchema.optional(),
|
|
204
|
+
autoTemplates: z.boolean().optional(),
|
|
205
|
+
title: z.string().max(4000).optional(),
|
|
206
|
+
})
|
|
207
|
+
.strict(),
|
|
208
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.convert_document,
|
|
209
|
+
annotations: ARTIFACT_CREATING,
|
|
210
|
+
}, async ({ source, targets, themeId, transformId, autoTemplates, title }, extra) => {
|
|
211
|
+
try {
|
|
212
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
213
|
+
const canonical = requireDocumentSource(source);
|
|
214
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
215
|
+
await sendProgress(extra, 0, targets.length + 1, 'Resolving document');
|
|
216
|
+
const prepared = await documents.prepare(canonical, operationSignal);
|
|
217
|
+
const results = await convertPreparedDocument(context.artifacts, prepared, {
|
|
218
|
+
targets: targets.map(toConversionTarget),
|
|
219
|
+
themeId,
|
|
220
|
+
transformId,
|
|
221
|
+
autoTemplates,
|
|
222
|
+
title,
|
|
223
|
+
}, operationSignal, (completed, total, message) => sendProgress(extra, completed + 1, total + 1, message));
|
|
224
|
+
for (const result of results)
|
|
225
|
+
requireWire(parseConversionResult(result), 'conversion');
|
|
226
|
+
const payload = { results };
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{ type: 'text', text: JSON.stringify(payload) },
|
|
230
|
+
...results.map((result) => artifactLink(result.artifact)),
|
|
231
|
+
],
|
|
232
|
+
structuredContent: asStructuredContent(successResult(payload)),
|
|
233
|
+
};
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
catch (caught) {
|
|
237
|
+
return errorResult(caught, 'convert', null, extra.signal);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
server.registerTool('create_document_bundle', {
|
|
241
|
+
description: 'Package Markdown and authority-scoped assets into an immutable DBK artifact with asset metadata.',
|
|
242
|
+
inputSchema: z.object({ source: bundleDocumentSourceSchema }).strict(),
|
|
243
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.create_document_bundle,
|
|
244
|
+
annotations: ARTIFACT_CREATING,
|
|
245
|
+
}, async ({ source }, extra) => {
|
|
246
|
+
try {
|
|
247
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
248
|
+
const canonical = requireDocumentSource(source);
|
|
249
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
250
|
+
const prepared = await documents.prepare(canonical, operationSignal);
|
|
251
|
+
const [result] = await convertPreparedDocument(context.artifacts, prepared, { targets: [{ format: 'dbk', fidelity: 'semantic' }] }, operationSignal);
|
|
252
|
+
if (!result)
|
|
253
|
+
throw new Error('DBK conversion produced no artifact');
|
|
254
|
+
requireWire(parseConversionResult(result), 'conversion');
|
|
255
|
+
return {
|
|
256
|
+
content: [
|
|
257
|
+
{ type: 'text', text: JSON.stringify(result) },
|
|
258
|
+
artifactLink(result.artifact),
|
|
259
|
+
],
|
|
260
|
+
structuredContent: asStructuredContent(successResult(result)),
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (caught) {
|
|
265
|
+
return errorResult(caught, 'convert', null, extra.signal);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
server.registerTool('save_artifact', {
|
|
269
|
+
description: 'Materialize an immutable artifact below a configured write root. Creation is no-replace; replacement requires the current SHA-256.',
|
|
270
|
+
inputSchema: z
|
|
271
|
+
.object({ artifactUri: artifactUriSchema, destination: materializationOptionsSchema })
|
|
272
|
+
.strict(),
|
|
273
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.save_artifact,
|
|
274
|
+
annotations: MATERIALIZING,
|
|
275
|
+
}, async ({ artifactUri, destination }, extra) => {
|
|
276
|
+
try {
|
|
277
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
278
|
+
const canonical = requireWire(parseMaterializationOptions(destination), 'materialization options');
|
|
279
|
+
const [authority, artifact, bytes] = await Promise.all([
|
|
280
|
+
context.authority,
|
|
281
|
+
context.artifacts.get(artifactUri, operationSignal),
|
|
282
|
+
context.artifacts.read(artifactUri, operationSignal),
|
|
283
|
+
]);
|
|
284
|
+
throwIfAborted(operationSignal);
|
|
285
|
+
const outputPath = await authority.authorizeRootWrite(canonical.rootId, canonical.path);
|
|
286
|
+
await authority.materializeBytes(outputPath, bytes, bytes.byteLength, {
|
|
287
|
+
ifExists: canonical.ifExists,
|
|
288
|
+
expectedSha256: canonical.expectedSha256 ?? undefined,
|
|
289
|
+
signal: operationSignal,
|
|
290
|
+
});
|
|
291
|
+
const payload = {
|
|
292
|
+
artifact,
|
|
293
|
+
destination: { rootId: canonical.rootId, path: canonical.path },
|
|
294
|
+
sha256: artifact.sha256,
|
|
295
|
+
};
|
|
296
|
+
return textAndStructured(payload);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch (caught) {
|
|
300
|
+
return errorResult(caught, 'materialize', null, extra.signal);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
server.registerTool('inspect_document', {
|
|
304
|
+
description: 'Inspect any linked-registry document as bounded semantic structure, block provenance, assets, metadata, theme, and diagnostics.',
|
|
305
|
+
inputSchema: z
|
|
306
|
+
.object({
|
|
307
|
+
source: documentSourceSchema,
|
|
308
|
+
maxBlocks: z.number().int().min(1).max(2000).optional(),
|
|
309
|
+
cursor: z.string().max(256).nullable().optional(),
|
|
310
|
+
})
|
|
311
|
+
.strict(),
|
|
312
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.inspect_document,
|
|
313
|
+
annotations: READ_ONLY,
|
|
314
|
+
}, async ({ source, maxBlocks, cursor }, extra) => {
|
|
315
|
+
try {
|
|
316
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
317
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
318
|
+
const prepared = await documents.prepare(requireDocumentSource(source), operationSignal);
|
|
319
|
+
const result = await inspectPreparedDocument(documents, prepared, maxBlocks, cursor ?? null, operationSignal);
|
|
320
|
+
requireWire(parseInspectionResult(result), 'inspection');
|
|
321
|
+
return textAndStructured(result);
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
catch (caught) {
|
|
325
|
+
return errorResult(caught, 'inspect', null, extra.signal);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
server.registerTool('validate_document', {
|
|
329
|
+
description: 'Validate document structure, templates, annotations, assets, accessibility, and optional target-format fidelity before export.',
|
|
330
|
+
inputSchema: z
|
|
331
|
+
.object({
|
|
332
|
+
source: documentSourceSchema,
|
|
333
|
+
targetFormat: formatInputSchema.nullable().optional(),
|
|
334
|
+
})
|
|
335
|
+
.strict(),
|
|
336
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.validate_document,
|
|
337
|
+
annotations: READ_ONLY,
|
|
338
|
+
}, async ({ source, targetFormat }, extra) => {
|
|
339
|
+
try {
|
|
340
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
341
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
342
|
+
const prepared = await documents.prepare(requireDocumentSource(source), operationSignal);
|
|
343
|
+
const normalizedTargetFormat = targetFormat?.toLowerCase() ?? null;
|
|
344
|
+
const result = await validatePreparedDocument(documents, prepared, normalizedTargetFormat, operationSignal);
|
|
345
|
+
requireWire(parseValidationResult(result), 'validation');
|
|
346
|
+
return textAndStructured(result);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (caught) {
|
|
350
|
+
return errorResult(caught, 'validate', null, extra.signal);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
server.registerTool('compare_documents', {
|
|
354
|
+
description: 'Compare two supported documents semantically and report text, structure, table, media, metadata, and theme retention.',
|
|
355
|
+
inputSchema: z.object({ left: documentSourceSchema, right: documentSourceSchema }).strict(),
|
|
356
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.compare_documents,
|
|
357
|
+
annotations: READ_ONLY,
|
|
358
|
+
}, async ({ left, right }, extra) => {
|
|
359
|
+
try {
|
|
360
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
361
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
362
|
+
const [leftPrepared, rightPrepared] = await Promise.all([
|
|
363
|
+
documents.prepare(requireDocumentSource(left), operationSignal),
|
|
364
|
+
documents.prepare(requireDocumentSource(right), operationSignal),
|
|
365
|
+
]);
|
|
366
|
+
const result = await comparePreparedDocuments(documents, leftPrepared, rightPrepared, operationSignal);
|
|
367
|
+
requireWire(parseComparisonResult(result), 'comparison');
|
|
368
|
+
return textAndStructured(result);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
catch (caught) {
|
|
372
|
+
return errorResult(caught, 'compare', null, extra.signal);
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
server.registerTool('preview_document', {
|
|
376
|
+
description: 'Render bounded visual previews as immutable image artifacts and report previewBasis. Markdown/DBK use source-render, imported document artifacts use reconstructed-import, and MP4/GIF sources use a native-extracted first-frame JPEG. Supports pagination and protocol progress/cancellation.',
|
|
377
|
+
inputSchema: z
|
|
378
|
+
.object({
|
|
379
|
+
source: documentSourceSchema,
|
|
380
|
+
startIndex: z.number().int().nonnegative().max(100000).optional(),
|
|
381
|
+
maxItems: z.number().int().min(1).max(20).optional(),
|
|
382
|
+
width: z.number().int().min(160).max(1920).optional(),
|
|
383
|
+
height: z.number().int().min(90).max(1920).optional(),
|
|
384
|
+
})
|
|
385
|
+
.strict(),
|
|
386
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.preview_document,
|
|
387
|
+
annotations: ARTIFACT_CREATING,
|
|
388
|
+
}, async ({ source, startIndex, maxItems, width, height }, extra) => {
|
|
389
|
+
try {
|
|
390
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
391
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
392
|
+
await sendProgress(extra, 0, 1, 'Resolving document');
|
|
393
|
+
const documentSource = requireDocumentSource(source);
|
|
394
|
+
const request = { startIndex, maxItems, width, height };
|
|
395
|
+
const reportProgress = (completed, total, message) => sendProgress(extra, completed, total, message);
|
|
396
|
+
const result = (await isRenderedMediaPreviewSource(documentSource, context.artifacts))
|
|
397
|
+
? await previewVideoSource(context.artifacts, await documents.readBinarySource(documentSource, operationSignal), request, operationSignal, reportProgress, context.extractVideoThumbnail)
|
|
398
|
+
: await previewPreparedDocument(context.artifacts, await documents.prepare(documentSource, operationSignal), request, operationSignal, reportProgress);
|
|
399
|
+
requireWire(parsePreviewResult(result), 'preview');
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{ type: 'text', text: JSON.stringify(result) },
|
|
403
|
+
...result.items.map((item) => artifactLink(item.artifact)),
|
|
404
|
+
],
|
|
405
|
+
structuredContent: asStructuredContent(successResult(result)),
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
catch (caught) {
|
|
410
|
+
return errorResult(caught, 'render', null, extra.signal);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
registerTemplateTools(server, context);
|
|
414
|
+
registerThemeTools(server, context);
|
|
415
|
+
}
|
|
416
|
+
function registerArtifactResource(server, artifacts) {
|
|
417
|
+
const template = new ResourceTemplate('docblocks://artifacts/{id}', {
|
|
418
|
+
list: undefined,
|
|
419
|
+
complete: { id: (prefix) => artifacts.completeIds(prefix) },
|
|
420
|
+
});
|
|
421
|
+
server.registerResource('artifact', template, {
|
|
422
|
+
description: 'Session-scoped immutable DocBlocks artifact. Large artifacts must be materialized with save_artifact.',
|
|
423
|
+
}, async (uri, variables, extra) => {
|
|
424
|
+
const rawId = variables.id;
|
|
425
|
+
const id = Array.isArray(rawId) ? rawId[0] : rawId;
|
|
426
|
+
if (!id)
|
|
427
|
+
throw new Error('Artifact resource id is required');
|
|
428
|
+
const { ref, bytes } = await artifacts.readResource(id, extra.signal);
|
|
429
|
+
return {
|
|
430
|
+
contents: [
|
|
431
|
+
{
|
|
432
|
+
uri: uri.toString(),
|
|
433
|
+
mimeType: ref.mimeType,
|
|
434
|
+
...(ref.mimeType.startsWith('text/')
|
|
435
|
+
? { text: bytes.toString('utf8') }
|
|
436
|
+
: { blob: bytes.toString('base64') }),
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
};
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
function registerConversionReportResource(server, artifacts) {
|
|
443
|
+
const template = new ResourceTemplate('docblocks://reports/{id}', {
|
|
444
|
+
list: undefined,
|
|
445
|
+
complete: { id: (prefix) => artifacts.completeReportIds(prefix) },
|
|
446
|
+
});
|
|
447
|
+
server.registerResource('conversion-report', template, {
|
|
448
|
+
description: 'Original machine-readable diagnostics and provenance for a conversion-backed session artifact.',
|
|
449
|
+
mimeType: 'application/json',
|
|
450
|
+
}, async (uri, variables) => {
|
|
451
|
+
const rawId = variables.id;
|
|
452
|
+
const id = Array.isArray(rawId) ? rawId[0] : rawId;
|
|
453
|
+
if (!id)
|
|
454
|
+
throw new Error('Conversion report resource id is required');
|
|
455
|
+
const report = await artifacts.getConversionReport(id);
|
|
456
|
+
return {
|
|
457
|
+
contents: [
|
|
458
|
+
{
|
|
459
|
+
uri: uri.toString(),
|
|
460
|
+
mimeType: 'application/json',
|
|
461
|
+
text: JSON.stringify(report, null, 2),
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function registerAuthoringGuideResource(server) {
|
|
468
|
+
server.registerResource('authoring-guide', 'docblocks://authoring-guide', {
|
|
469
|
+
description: 'Compact linked-Squisq authoring vocabulary, artifact workflow, fidelity modes, templates, themes, and transform styles.',
|
|
470
|
+
mimeType: 'application/json',
|
|
471
|
+
}, async () => {
|
|
472
|
+
const [docModule, schemaModule, transformModule] = await Promise.all([
|
|
473
|
+
import('@bendyline/squisq/doc'),
|
|
474
|
+
import('@bendyline/squisq/schemas'),
|
|
475
|
+
import('@bendyline/squisq/transform'),
|
|
476
|
+
]);
|
|
477
|
+
const templates = docModule
|
|
478
|
+
.getAvailableTemplates()
|
|
479
|
+
.slice(0, MCP_WIRE_LIMITS.arrayEntries)
|
|
480
|
+
.map((id) => ({
|
|
481
|
+
id: requireWireIdentifier(id, 'template id'),
|
|
482
|
+
label: boundWireText(docModule.TEMPLATE_METADATA[id]?.label ?? id, MCP_WIRE_LIMITS.labelCharacters, id),
|
|
483
|
+
description: boundWireText(docModule.TEMPLATE_METADATA[id]?.description ?? ''),
|
|
484
|
+
inputs: (docModule.TEMPLATE_INPUT_DESCRIPTORS[id] ?? [])
|
|
485
|
+
.slice(0, MCP_WIRE_LIMITS.arrayEntries)
|
|
486
|
+
.map((input) => ({
|
|
487
|
+
key: requireWireIdentifier(input.key, 'template input key'),
|
|
488
|
+
required: input.required ?? false,
|
|
489
|
+
values: boundWireTextArray(input.values ?? [], MCP_WIRE_LIMITS.arrayEntries, MCP_WIRE_LIMITS.labelCharacters),
|
|
490
|
+
hint: boundNullableWireText(input.valueHint, MCP_WIRE_LIMITS.labelCharacters),
|
|
491
|
+
})),
|
|
492
|
+
}));
|
|
493
|
+
const guide = {
|
|
494
|
+
version: 1,
|
|
495
|
+
workflow: [
|
|
496
|
+
'list_roots and select an authority-scoped source',
|
|
497
|
+
'inspect_document and validate_document',
|
|
498
|
+
'convert_document to one or more immutable artifacts',
|
|
499
|
+
'preview_document and inspect previewBasis before treating images as source renders, reconstructed imports, or native extraction',
|
|
500
|
+
'save_artifact only when a durable file is required',
|
|
501
|
+
],
|
|
502
|
+
markdownAnnotation: '{[templateId key="value"]}',
|
|
503
|
+
fidelity: {
|
|
504
|
+
semantic: 'Prioritize meaning and portability.',
|
|
505
|
+
'editable-native': 'Produce editable native structures with target-specific diagnostics.',
|
|
506
|
+
'rendered-fidelity': 'Preserve Squisq pixels in rendered slide/page media.',
|
|
507
|
+
hybrid: 'Preserve rendered visuals plus bounded machine-extractable semantic retention; current PPTX text is hidden and current PDF is not a tagged accessible PDF.',
|
|
508
|
+
},
|
|
509
|
+
fidelityByFormat: MCP_FORMAT_FIDELITIES,
|
|
510
|
+
formats: boundedFormatCapabilities(),
|
|
511
|
+
templates,
|
|
512
|
+
themes: schemaModule
|
|
513
|
+
.getThemeSummaries()
|
|
514
|
+
.slice(0, MCP_WIRE_LIMITS.arrayEntries)
|
|
515
|
+
.map((theme) => ({
|
|
516
|
+
id: requireWireIdentifier(theme.id, 'theme id'),
|
|
517
|
+
name: boundWireText(theme.name, MCP_WIRE_LIMITS.labelCharacters, theme.id),
|
|
518
|
+
description: boundWireText(theme.description ?? ''),
|
|
519
|
+
})),
|
|
520
|
+
transformStyles: transformModule
|
|
521
|
+
.getTransformStyleSummaries()
|
|
522
|
+
.slice(0, MCP_WIRE_LIMITS.arrayEntries)
|
|
523
|
+
.map((style) => ({
|
|
524
|
+
id: requireWireIdentifier(style.id, 'transform style id'),
|
|
525
|
+
name: boundWireText(style.name, MCP_WIRE_LIMITS.labelCharacters, style.id),
|
|
526
|
+
description: boundWireText(style.description),
|
|
527
|
+
})),
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
contents: [
|
|
531
|
+
{
|
|
532
|
+
uri: 'docblocks://authoring-guide',
|
|
533
|
+
mimeType: 'application/json',
|
|
534
|
+
text: JSON.stringify(guide, null, 2),
|
|
535
|
+
},
|
|
536
|
+
],
|
|
537
|
+
};
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function registerTemplateTools(server, context) {
|
|
541
|
+
server.registerTool('list_templates', {
|
|
542
|
+
description: 'List Squisq authoring templates agents can use in Markdown annotations.',
|
|
543
|
+
inputSchema: z.object({}).strict(),
|
|
544
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.list_templates,
|
|
545
|
+
annotations: READ_ONLY,
|
|
546
|
+
}, async () => {
|
|
547
|
+
try {
|
|
548
|
+
const { getAvailableTemplates, TEMPLATE_METADATA } = await import('@bendyline/squisq/doc');
|
|
549
|
+
const templates = getAvailableTemplates()
|
|
550
|
+
.slice(0, MCP_WIRE_LIMITS.arrayEntries)
|
|
551
|
+
.map((id) => ({
|
|
552
|
+
id: requireWireIdentifier(id, 'template id'),
|
|
553
|
+
label: boundWireText(TEMPLATE_METADATA[id]?.label ?? id, MCP_WIRE_LIMITS.labelCharacters, id),
|
|
554
|
+
description: boundWireText(TEMPLATE_METADATA[id]?.description ?? ''),
|
|
555
|
+
}));
|
|
556
|
+
return textAndStructured({ templates });
|
|
557
|
+
}
|
|
558
|
+
catch (caught) {
|
|
559
|
+
return errorResult(caught, 'inspect');
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
server.registerTool('describe_template', {
|
|
563
|
+
description: 'Describe one Squisq template and its exact Markdown annotation inputs.',
|
|
564
|
+
inputSchema: z.object({ templateId: identifierSchema }).strict(),
|
|
565
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.describe_template,
|
|
566
|
+
annotations: READ_ONLY,
|
|
567
|
+
}, async ({ templateId }) => {
|
|
568
|
+
try {
|
|
569
|
+
const { getAvailableTemplates, TEMPLATE_INPUT_DESCRIPTORS, TEMPLATE_METADATA } = await import('@bendyline/squisq/doc');
|
|
570
|
+
if (!getAvailableTemplates().includes(templateId)) {
|
|
571
|
+
return errorResult(new Error(`Unknown template "${templateId}"`), 'validate');
|
|
572
|
+
}
|
|
573
|
+
const inputs = (TEMPLATE_INPUT_DESCRIPTORS[templateId] ?? [])
|
|
574
|
+
.slice(0, MCP_WIRE_LIMITS.arrayEntries)
|
|
575
|
+
.map((input) => ({
|
|
576
|
+
key: requireWireIdentifier(input.key, 'template input key'),
|
|
577
|
+
description: boundWireText(input.description),
|
|
578
|
+
type: boundWireText(input.coerce ?? 'string', MCP_WIRE_LIMITS.labelCharacters, 'string'),
|
|
579
|
+
required: input.required ?? false,
|
|
580
|
+
values: boundWireTextArray(input.values ?? [], MCP_WIRE_LIMITS.arrayEntries, MCP_WIRE_LIMITS.labelCharacters),
|
|
581
|
+
valueHint: boundNullableWireText(input.valueHint, MCP_WIRE_LIMITS.labelCharacters),
|
|
582
|
+
}));
|
|
583
|
+
const metadata = TEMPLATE_METADATA[templateId];
|
|
584
|
+
const template = {
|
|
585
|
+
id: requireWireIdentifier(templateId, 'template id'),
|
|
586
|
+
label: boundWireText(metadata?.label ?? templateId, MCP_WIRE_LIMITS.labelCharacters, templateId),
|
|
587
|
+
description: boundWireText(metadata?.description ?? ''),
|
|
588
|
+
inputs,
|
|
589
|
+
};
|
|
590
|
+
const required = inputs
|
|
591
|
+
.filter((input) => input.required)
|
|
592
|
+
.map((input) => `${input.key}="…"`)
|
|
593
|
+
.join(' ');
|
|
594
|
+
return textAndStructured({
|
|
595
|
+
template,
|
|
596
|
+
annotationExample: boundWireText(`{[${templateId}${required ? ` ${required}` : ''}]}`),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
catch (caught) {
|
|
600
|
+
return errorResult(caught, 'inspect');
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
server.registerTool('recommend_templates', {
|
|
604
|
+
description: 'Profile each bounded document block with the linked Squisq recommender and return content-compatible template candidates.',
|
|
605
|
+
inputSchema: z
|
|
606
|
+
.object({
|
|
607
|
+
source: documentSourceSchema,
|
|
608
|
+
candidateTemplateIds: z.array(identifierSchema).max(256).optional(),
|
|
609
|
+
maxBlocks: z.number().int().min(1).max(100).optional(),
|
|
610
|
+
})
|
|
611
|
+
.strict(),
|
|
612
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.recommend_templates,
|
|
613
|
+
annotations: READ_ONLY,
|
|
614
|
+
}, async ({ source, candidateTemplateIds, maxBlocks }, extra) => {
|
|
615
|
+
try {
|
|
616
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
617
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
618
|
+
const prepared = await documents.prepare(requireDocumentSource(source), operationSignal);
|
|
619
|
+
const [{ flattenBlocks, getAvailableTemplates }, recommendModule] = await Promise.all([
|
|
620
|
+
import('@bendyline/squisq/doc'),
|
|
621
|
+
import('@bendyline/squisq/recommend'),
|
|
622
|
+
]);
|
|
623
|
+
const available = getAvailableTemplates();
|
|
624
|
+
const candidates = candidateTemplateIds ?? available;
|
|
625
|
+
const unknown = candidates.find((id) => !available.includes(id));
|
|
626
|
+
if (unknown)
|
|
627
|
+
throw new Error(`Unknown template "${unknown}"`);
|
|
628
|
+
const blocks = flattenBlocks(prepared.doc.blocks);
|
|
629
|
+
const limit = maxBlocks ?? 50;
|
|
630
|
+
const recommendations = blocks.slice(0, limit).map((block) => {
|
|
631
|
+
const profile = recommendModule.profileBlockContents(block.contents ?? []);
|
|
632
|
+
return {
|
|
633
|
+
blockId: toWireIdentifier(block.id, 'block'),
|
|
634
|
+
title: boundNullableWireText(block.title, MCP_WIRE_LIMITS.labelCharacters),
|
|
635
|
+
profile: {
|
|
636
|
+
...profile,
|
|
637
|
+
hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
|
|
638
|
+
hasTimeline: profile.hasTimeline ?? false,
|
|
639
|
+
hasTree: profile.hasTree ?? false,
|
|
640
|
+
},
|
|
641
|
+
recommendedTemplateIds: recommendModule
|
|
642
|
+
.recommendTemplatesForBlock(profile, candidates)
|
|
643
|
+
.recommended.slice(0, 256)
|
|
644
|
+
.map((id) => requireWireIdentifier(id, 'recommended template id')),
|
|
645
|
+
};
|
|
646
|
+
});
|
|
647
|
+
return textAndStructured({
|
|
648
|
+
recommendations,
|
|
649
|
+
totalBlocks: blocks.length,
|
|
650
|
+
truncated: recommendations.length < blocks.length,
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
catch (caught) {
|
|
655
|
+
return errorResult(caught, 'inspect', null, extra.signal);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
function registerThemeTools(server, context) {
|
|
660
|
+
server.registerTool('describe_theme', {
|
|
661
|
+
description: 'Describe one built-in or document-scoped custom Squisq theme before conversion.',
|
|
662
|
+
inputSchema: z
|
|
663
|
+
.object({
|
|
664
|
+
themeId: identifierSchema,
|
|
665
|
+
source: documentSourceSchema.optional(),
|
|
666
|
+
})
|
|
667
|
+
.strict(),
|
|
668
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.describe_theme,
|
|
669
|
+
annotations: READ_ONLY,
|
|
670
|
+
}, async ({ themeId, source }, extra) => {
|
|
671
|
+
try {
|
|
672
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
673
|
+
const [docModule, schemaModule] = await Promise.all([
|
|
674
|
+
import('@bendyline/squisq/doc'),
|
|
675
|
+
import('@bendyline/squisq/schemas'),
|
|
676
|
+
]);
|
|
677
|
+
const prepared = source
|
|
678
|
+
? await new DocumentService(await context.authority, context.artifacts).prepare(requireDocumentSource(source), operationSignal)
|
|
679
|
+
: null;
|
|
680
|
+
throwIfAborted(operationSignal);
|
|
681
|
+
const documentTheme = prepared?.doc.customThemes?.find((theme) => theme.id === themeId);
|
|
682
|
+
const builtIn = Object.prototype.hasOwnProperty.call(schemaModule.THEMES, themeId);
|
|
683
|
+
if (!documentTheme && !builtIn) {
|
|
684
|
+
return errorResult(new Error(`Unknown theme "${themeId}"`), 'validate');
|
|
685
|
+
}
|
|
686
|
+
const theme = docModule.resolveThemeForDoc(prepared?.doc, themeId);
|
|
687
|
+
return textAndStructured({
|
|
688
|
+
theme: summarizeTheme(theme),
|
|
689
|
+
source: documentTheme ? 'document' : 'built-in',
|
|
690
|
+
});
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
catch (caught) {
|
|
694
|
+
return errorResult(caught, 'inspect', null, extra.signal);
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
server.registerTool('infer_theme_from_file', {
|
|
698
|
+
description: 'Infer a reusable Squisq theme and optional custom layout summaries from a DOCX, PPTX, or XLSX file/artifact.',
|
|
699
|
+
inputSchema: z
|
|
700
|
+
.object({ source: documentSourceSchema, inferLayouts: z.boolean().optional() })
|
|
701
|
+
.strict(),
|
|
702
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.infer_theme_from_file,
|
|
703
|
+
annotations: READ_ONLY,
|
|
704
|
+
}, async ({ source, inferLayouts }, extra) => {
|
|
705
|
+
try {
|
|
706
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
707
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
708
|
+
const binary = await documents.readBinarySource(requireDocumentSource(source), operationSignal);
|
|
709
|
+
if (!['docx', 'pptx', 'xlsx'].includes(binary.format)) {
|
|
710
|
+
throw new Error('Theme inference requires DOCX, PPTX, or XLSX input');
|
|
711
|
+
}
|
|
712
|
+
const { inferThemeFromFile } = await import('@bendyline/squisq-formats');
|
|
713
|
+
const inferred = await inferThemeFromFile(ownedArrayBuffer(binary.bytes), {
|
|
714
|
+
signal: operationSignal,
|
|
715
|
+
format: binary.format,
|
|
716
|
+
inferLayouts,
|
|
717
|
+
nameHint: binary.filename,
|
|
718
|
+
maxEntries: 2048,
|
|
719
|
+
maxEntryUncompressedBytes: 100 * 1024 * 1024,
|
|
720
|
+
maxUncompressedBytes: 100 * 1024 * 1024,
|
|
721
|
+
});
|
|
722
|
+
throwIfAborted(operationSignal);
|
|
723
|
+
const payload = {
|
|
724
|
+
sourceFormat: binary.format,
|
|
725
|
+
sourceSha256: binary.sha256,
|
|
726
|
+
theme: summarizeTheme(inferred.theme),
|
|
727
|
+
layouts: (inferred.layouts ?? []).slice(0, 1000).map((layout, index) => ({
|
|
728
|
+
id: boundWireText(layout.name, MCP_WIRE_LIMITS.labelCharacters, `layout-${index + 1}`),
|
|
729
|
+
name: boundWireText(layout.label, MCP_WIRE_LIMITS.labelCharacters, `Layout ${index + 1}`),
|
|
730
|
+
description: boundWireText(layout.description ?? ''),
|
|
731
|
+
})),
|
|
732
|
+
warnings: boundWireWarnings(inferred.warnings),
|
|
733
|
+
};
|
|
734
|
+
return textAndStructured(payload);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
catch (caught) {
|
|
738
|
+
return errorResult(caught, 'inspect', null, extra.signal);
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
server.registerTool('inspect_pptx_layouts', {
|
|
742
|
+
description: 'Inspect PowerPoint slide masters/layouts and show built-in matches or inferred custom-template candidates.',
|
|
743
|
+
inputSchema: z.object({ source: documentSourceSchema }).strict(),
|
|
744
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.inspect_pptx_layouts,
|
|
745
|
+
annotations: READ_ONLY,
|
|
746
|
+
}, async ({ source }, extra) => {
|
|
747
|
+
try {
|
|
748
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
749
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
750
|
+
const binary = await documents.readBinarySource(requireDocumentSource(source), operationSignal);
|
|
751
|
+
if (binary.format !== 'pptx')
|
|
752
|
+
throw new Error('PPTX layout inspection requires PPTX input');
|
|
753
|
+
const { inspectPptxLayouts } = await import('@bendyline/squisq-formats/pptx');
|
|
754
|
+
const result = await inspectPptxLayouts(ownedArrayBuffer(binary.bytes), {
|
|
755
|
+
signal: operationSignal,
|
|
756
|
+
maxEntries: 2048,
|
|
757
|
+
maxEntryUncompressedBytes: 100 * 1024 * 1024,
|
|
758
|
+
maxUncompressedBytes: 100 * 1024 * 1024,
|
|
759
|
+
});
|
|
760
|
+
throwIfAborted(operationSignal);
|
|
761
|
+
return textAndStructured({
|
|
762
|
+
slideSize: result.slideSize,
|
|
763
|
+
layouts: result.layouts.slice(0, 2048).map((layout, index) => ({
|
|
764
|
+
layoutPath: boundWireText(layout.layoutPath, MCP_WIRE_LIMITS.pathCharacters, `ppt/slideLayouts/slideLayout${index + 1}.xml`),
|
|
765
|
+
name: boundWireText(layout.name, MCP_WIRE_LIMITS.labelCharacters, `Layout ${index + 1}`),
|
|
766
|
+
masterName: boundNullableWireText(layout.masterName, MCP_WIRE_LIMITS.labelCharacters),
|
|
767
|
+
type: boundNullableWireText(layout.typeAttr, MCP_WIRE_LIMITS.labelCharacters),
|
|
768
|
+
slideCount: layout.slideCount,
|
|
769
|
+
verdict: layout.verdict,
|
|
770
|
+
templateId: boundNullableWireText(layout.builtinTemplate ?? layout.customTemplate?.name, MCP_WIRE_LIMITS.labelCharacters),
|
|
771
|
+
notes: boundWireTextArray(layout.notes ?? [], 1000),
|
|
772
|
+
})),
|
|
773
|
+
});
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
catch (caught) {
|
|
777
|
+
return errorResult(caught, 'inspect', null, extra.signal);
|
|
778
|
+
}
|
|
779
|
+
});
|
|
780
|
+
server.registerTool('apply_inferred_theme', {
|
|
781
|
+
description: 'Infer a theme from a DOCX, PPTX, or XLSX reference and embed it, optional custom layouts, and all source assets into a reusable DBK artifact.',
|
|
782
|
+
inputSchema: z
|
|
783
|
+
.object({
|
|
784
|
+
source: documentSourceSchema,
|
|
785
|
+
themeSource: documentSourceSchema,
|
|
786
|
+
inferLayouts: z.boolean().optional(),
|
|
787
|
+
})
|
|
788
|
+
.strict(),
|
|
789
|
+
outputSchema: DOCBLOCKS_MCP_TOOL_OUTPUT_SCHEMAS.apply_inferred_theme,
|
|
790
|
+
annotations: ARTIFACT_CREATING,
|
|
791
|
+
}, async ({ source, themeSource, inferLayouts }, extra) => {
|
|
792
|
+
try {
|
|
793
|
+
return await context.runOperation(extra.signal, async (operationSignal) => {
|
|
794
|
+
const documents = new DocumentService(await context.authority, context.artifacts);
|
|
795
|
+
const [prepared, binary] = await Promise.all([
|
|
796
|
+
documents.prepare(requireDocumentSource(source), operationSignal),
|
|
797
|
+
documents.readBinarySource(requireDocumentSource(themeSource), operationSignal),
|
|
798
|
+
]);
|
|
799
|
+
throwIfAborted(operationSignal);
|
|
800
|
+
if (!['docx', 'pptx', 'xlsx'].includes(binary.format)) {
|
|
801
|
+
throw new Error('Theme application requires DOCX, PPTX, or XLSX theme input');
|
|
802
|
+
}
|
|
803
|
+
const { inferThemeFromFile } = await import('@bendyline/squisq-formats');
|
|
804
|
+
const inferred = await inferThemeFromFile(ownedArrayBuffer(binary.bytes), {
|
|
805
|
+
signal: operationSignal,
|
|
806
|
+
format: binary.format,
|
|
807
|
+
inferLayouts,
|
|
808
|
+
nameHint: binary.filename,
|
|
809
|
+
maxEntries: 2048,
|
|
810
|
+
maxEntryUncompressedBytes: 100 * 1024 * 1024,
|
|
811
|
+
maxUncompressedBytes: 100 * 1024 * 1024,
|
|
812
|
+
});
|
|
813
|
+
throwIfAborted(operationSignal);
|
|
814
|
+
const customThemes = [
|
|
815
|
+
...(prepared.doc.customThemes ?? []).filter((theme) => theme.id !== inferred.theme.id),
|
|
816
|
+
inferred.theme,
|
|
817
|
+
];
|
|
818
|
+
const inferredLayoutNames = new Set((inferred.layouts ?? []).map((layout) => layout.name));
|
|
819
|
+
const customTemplates = [
|
|
820
|
+
...(prepared.doc.customTemplates ?? []).filter((template) => !inferredLayoutNames.has(template.name)),
|
|
821
|
+
...(inferred.layouts ?? []),
|
|
822
|
+
];
|
|
823
|
+
const themedDoc = {
|
|
824
|
+
...prepared.doc,
|
|
825
|
+
themeId: inferred.theme.id,
|
|
826
|
+
customThemes,
|
|
827
|
+
customTemplates,
|
|
828
|
+
frontmatter: {
|
|
829
|
+
...(prepared.doc.frontmatter ?? {}),
|
|
830
|
+
'squisq-theme': inferred.theme.id,
|
|
831
|
+
},
|
|
832
|
+
};
|
|
833
|
+
const [{ docToMarkdown }, { stringifyMarkdown }] = await Promise.all([
|
|
834
|
+
import('@bendyline/squisq/doc'),
|
|
835
|
+
import('@bendyline/squisq/markdown'),
|
|
836
|
+
]);
|
|
837
|
+
const markdownDoc = docToMarkdown(themedDoc);
|
|
838
|
+
const markdown = stringifyMarkdown(markdownDoc);
|
|
839
|
+
const [result] = await convertPreparedDocument(context.artifacts, {
|
|
840
|
+
...prepared,
|
|
841
|
+
doc: themedDoc,
|
|
842
|
+
markdownDoc,
|
|
843
|
+
markdown,
|
|
844
|
+
diagnostics: [
|
|
845
|
+
...prepared.diagnostics,
|
|
846
|
+
...inferred.warnings.map((warning) => warningDiagnostic(warning, 'transform', binary.format)),
|
|
847
|
+
],
|
|
848
|
+
}, {
|
|
849
|
+
themeId: inferred.theme.id,
|
|
850
|
+
targets: [{ format: 'dbk', fidelity: 'semantic' }],
|
|
851
|
+
}, operationSignal);
|
|
852
|
+
if (!result)
|
|
853
|
+
throw new Error('Themed DBK conversion produced no artifact');
|
|
854
|
+
requireWire(parseConversionResult(result), 'conversion');
|
|
855
|
+
const payload = {
|
|
856
|
+
result,
|
|
857
|
+
theme: summarizeTheme(inferred.theme),
|
|
858
|
+
layoutIds: (inferred.layouts ?? [])
|
|
859
|
+
.slice(0, 1000)
|
|
860
|
+
.map((layout) => boundWireText(layout.name, MCP_WIRE_LIMITS.labelCharacters)),
|
|
861
|
+
warnings: boundWireWarnings(inferred.warnings),
|
|
862
|
+
};
|
|
863
|
+
return {
|
|
864
|
+
content: [
|
|
865
|
+
{ type: 'text', text: JSON.stringify(payload) },
|
|
866
|
+
artifactLink(result.artifact),
|
|
867
|
+
],
|
|
868
|
+
structuredContent: asStructuredContent(successResult(payload)),
|
|
869
|
+
};
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
catch (caught) {
|
|
873
|
+
return errorResult(caught, 'transform', null, extra.signal);
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function requireDocumentSource(value) {
|
|
878
|
+
return requireWire(parseDocumentSource(value), 'document source');
|
|
879
|
+
}
|
|
880
|
+
async function isRenderedMediaPreviewSource(source, artifacts) {
|
|
881
|
+
if (source.kind === 'file') {
|
|
882
|
+
const format = source.format?.toLowerCase().replace(/^\./u, '');
|
|
883
|
+
return format && format !== 'auto'
|
|
884
|
+
? format === 'mp4' || format === 'gif'
|
|
885
|
+
: /\.(?:mp4|gif)$/iu.test(source.path);
|
|
886
|
+
}
|
|
887
|
+
if (source.kind !== 'artifact')
|
|
888
|
+
return false;
|
|
889
|
+
const artifact = await artifacts.get(source.uri);
|
|
890
|
+
const format = artifact.format.toLowerCase().replace(/^\./u, '');
|
|
891
|
+
return format === 'mp4' || format === 'gif';
|
|
892
|
+
}
|
|
893
|
+
function requireWire(value, label) {
|
|
894
|
+
if (value === null)
|
|
895
|
+
throw new Error(`Invalid canonical MCP ${label}`);
|
|
896
|
+
return value;
|
|
897
|
+
}
|
|
898
|
+
function toConversionTarget(value) {
|
|
899
|
+
const { format, fidelity, ...options } = value;
|
|
900
|
+
return {
|
|
901
|
+
format,
|
|
902
|
+
fidelity,
|
|
903
|
+
options: Object.fromEntries(Object.entries(options).filter((entry) => entry[1] !== undefined)),
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function artifactLink(artifact) {
|
|
907
|
+
return {
|
|
908
|
+
type: 'resource_link',
|
|
909
|
+
uri: artifact.uri,
|
|
910
|
+
name: artifact.suggestedFilename,
|
|
911
|
+
description: `${artifact.format} artifact (${artifact.sha256})`,
|
|
912
|
+
mimeType: artifact.mimeType,
|
|
913
|
+
size: artifact.size,
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
async function sendProgress(extra, progress, total, message) {
|
|
917
|
+
await reportMcpProgress(extra, progress, total, message);
|
|
918
|
+
}
|
|
919
|
+
function textAndStructured(payload) {
|
|
920
|
+
return {
|
|
921
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
|
|
922
|
+
structuredContent: asStructuredContent(successResult(payload)),
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function asStructuredContent(payload) {
|
|
926
|
+
return payload;
|
|
927
|
+
}
|
|
928
|
+
function ownedArrayBuffer(bytes) {
|
|
929
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
930
|
+
copy.set(bytes);
|
|
931
|
+
return copy.buffer;
|
|
932
|
+
}
|
|
933
|
+
function summarizeTheme(theme) {
|
|
934
|
+
const id = requireWireIdentifier(theme.id, 'theme id');
|
|
935
|
+
return {
|
|
936
|
+
id,
|
|
937
|
+
name: boundWireText(theme.name, MCP_WIRE_LIMITS.labelCharacters, id),
|
|
938
|
+
description: boundWireText(theme.description ?? ''),
|
|
939
|
+
colors: {
|
|
940
|
+
primary: boundWireText(theme.colors.primary, MCP_WIRE_LIMITS.labelCharacters, '#000000'),
|
|
941
|
+
secondary: boundWireText(theme.colors.secondary, MCP_WIRE_LIMITS.labelCharacters, '#000000'),
|
|
942
|
+
background: boundWireText(theme.colors.background, MCP_WIRE_LIMITS.labelCharacters, '#ffffff'),
|
|
943
|
+
text: boundWireText(theme.colors.text, MCP_WIRE_LIMITS.labelCharacters, '#000000'),
|
|
944
|
+
highlight: boundWireText(theme.colors.highlight, MCP_WIRE_LIMITS.labelCharacters, '#000000'),
|
|
945
|
+
},
|
|
946
|
+
bodyFont: boundWireText(JSON.stringify(theme.typography.bodyFont) ?? ''),
|
|
947
|
+
titleFont: boundWireText(JSON.stringify(theme.typography.titleFont) ?? ''),
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
function boundedFormatCapabilities() {
|
|
951
|
+
return MCP_FORMAT_CAPABILITIES.slice(0, 64).map((format) => {
|
|
952
|
+
const id = requireWireIdentifier(format.id, 'format id');
|
|
953
|
+
if (id.length > MCP_WIRE_LIMITS.formatCharacters || !MCP_OUTPUT_PATTERNS.format.test(id)) {
|
|
954
|
+
throw new Error('Linked Squisq returned an invalid MCP format id');
|
|
955
|
+
}
|
|
956
|
+
return {
|
|
957
|
+
id,
|
|
958
|
+
label: boundWireText(format.label, MCP_WIRE_LIMITS.labelCharacters, id),
|
|
959
|
+
mimeType: format.mimeType,
|
|
960
|
+
extensions: boundWireTextArray(format.extensions, 64, MCP_WIRE_LIMITS.labelCharacters),
|
|
961
|
+
import: { ...format.import },
|
|
962
|
+
export: { ...format.export },
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
export { MCP_FORMAT_CAPABILITIES };
|
|
967
|
+
//# sourceMappingURL=agentic-tools.js.map
|