@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
package/dist/mcp/server.js
CHANGED
|
@@ -1,517 +1,249 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DocBlocks MCP Server
|
|
3
|
-
*
|
|
4
|
-
* Exposes document conversion, analysis, and transformation tools
|
|
5
|
-
* via the Model Context Protocol (MCP) over stdio.
|
|
6
|
-
*
|
|
7
|
-
* Designed for AI agents: tools accept raw markdown text so agents
|
|
8
|
-
* can write content and immediately export it without temp files.
|
|
9
|
-
*/
|
|
10
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
2
|
+
import { getPackageVersion } from '../version.js';
|
|
3
|
+
import { registerAgenticTools, MCP_CONVERSION_TARGET_FORMATS, MCP_FORMAT_CAPABILITIES, } from './agentic-tools.js';
|
|
4
|
+
import { ArtifactStore } from './artifact-store.js';
|
|
5
|
+
import { McpFileAuthority } from './authority.js';
|
|
6
|
+
import { registerDiscoveryTools } from './discovery-tools.js';
|
|
7
|
+
import { registerAuthoringPrompts } from './prompts.js';
|
|
8
|
+
const DEFAULT_MAX_CONCURRENT_OPERATIONS = 2;
|
|
9
|
+
const DEFAULT_OPERATION_TIMEOUT_MS = 2 * 60 * 1000;
|
|
10
|
+
const DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS = 5000;
|
|
11
|
+
/** Bounds expensive work while preserving the exact caller cancellation reason. */
|
|
12
|
+
export class OperationGuard {
|
|
13
|
+
constructor(maximum, timeoutMs, shutdownDrainTimeoutMs = DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS) {
|
|
14
|
+
this.maximum = maximum;
|
|
15
|
+
this.timeoutMs = timeoutMs;
|
|
16
|
+
this.shutdownDrainTimeoutMs = shutdownDrainTimeoutMs;
|
|
17
|
+
this.active = 0;
|
|
18
|
+
this.accepting = true;
|
|
19
|
+
this.controllers = new Set();
|
|
20
|
+
this.completions = new Set();
|
|
21
|
+
this.shutdownPromise = null;
|
|
22
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > 32) {
|
|
23
|
+
throw new Error('Invalid MCP operation concurrency limit');
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 10 || timeoutMs > 30 * 60 * 1000) {
|
|
26
|
+
throw new Error('Invalid MCP operation timeout');
|
|
27
|
+
}
|
|
28
|
+
if (!Number.isSafeInteger(shutdownDrainTimeoutMs) ||
|
|
29
|
+
shutdownDrainTimeoutMs < 10 ||
|
|
30
|
+
shutdownDrainTimeoutMs > 60000) {
|
|
31
|
+
throw new Error('Invalid MCP shutdown drain timeout');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async run(requestSignal, work) {
|
|
35
|
+
requestSignal.throwIfAborted();
|
|
36
|
+
if (!this.accepting)
|
|
37
|
+
throw new OperationClosingError();
|
|
38
|
+
if (this.active >= this.maximum) {
|
|
39
|
+
throw new OperationBusyError(this.active, this.maximum);
|
|
40
|
+
}
|
|
41
|
+
this.active += 1;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
this.controllers.add(controller);
|
|
44
|
+
const forwardCancellation = () => controller.abort(requestSignal.reason);
|
|
45
|
+
if (requestSignal.aborted)
|
|
46
|
+
forwardCancellation();
|
|
47
|
+
else
|
|
48
|
+
requestSignal.addEventListener('abort', forwardCancellation, { once: true });
|
|
49
|
+
const timer = setTimeout(() => controller.abort(new OperationTimeoutError(this.timeoutMs)), this.timeoutMs);
|
|
50
|
+
let rejectAborted = () => undefined;
|
|
51
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
52
|
+
rejectAborted = () => reject(controller.signal.reason ?? new Error('MCP operation was cancelled'));
|
|
53
|
+
if (controller.signal.aborted)
|
|
54
|
+
rejectAborted();
|
|
55
|
+
else
|
|
56
|
+
controller.signal.addEventListener('abort', rejectAborted, { once: true });
|
|
57
|
+
});
|
|
58
|
+
const completion = Promise.resolve()
|
|
59
|
+
.then(() => work(controller.signal))
|
|
60
|
+
.finally(() => {
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
requestSignal.removeEventListener('abort', forwardCancellation);
|
|
63
|
+
controller.signal.removeEventListener('abort', rejectAborted);
|
|
64
|
+
this.controllers.delete(controller);
|
|
65
|
+
this.completions.delete(completion);
|
|
66
|
+
this.active -= 1;
|
|
67
|
+
});
|
|
68
|
+
this.completions.add(completion);
|
|
23
69
|
try {
|
|
24
|
-
|
|
25
|
-
const info = await stat(resolved);
|
|
26
|
-
if (info.isFile() || info.isDirectory()) {
|
|
27
|
-
return { filePath: resolved, isTemp: false };
|
|
28
|
-
}
|
|
70
|
+
return await Promise.race([completion, aborted]);
|
|
29
71
|
}
|
|
30
|
-
catch {
|
|
31
|
-
|
|
72
|
+
catch (caught) {
|
|
73
|
+
if (controller.signal.aborted) {
|
|
74
|
+
// Give abort-aware work a turn to release resources. Work that ignores
|
|
75
|
+
// cancellation still occupies its slot without delaying the response.
|
|
76
|
+
await Promise.race([
|
|
77
|
+
completion.then(() => undefined, () => undefined),
|
|
78
|
+
new Promise((resolve) => setTimeout(resolve, 0)),
|
|
79
|
+
]);
|
|
80
|
+
throw controller.signal.reason ?? caught;
|
|
81
|
+
}
|
|
82
|
+
throw caught;
|
|
32
83
|
}
|
|
33
84
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
await rm(filePath, { force: true });
|
|
85
|
+
/** Stop admission, abort active work, and wait for all operation cleanup before disposal. */
|
|
86
|
+
shutdown() {
|
|
87
|
+
if (this.shutdownPromise)
|
|
88
|
+
return this.shutdownPromise;
|
|
89
|
+
this.accepting = false;
|
|
90
|
+
const attempt = this.drain();
|
|
91
|
+
this.shutdownPromise = attempt.catch((caught) => {
|
|
92
|
+
this.shutdownPromise = null;
|
|
93
|
+
throw caught;
|
|
94
|
+
});
|
|
95
|
+
return this.shutdownPromise;
|
|
46
96
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
97
|
+
async drain() {
|
|
98
|
+
const reason = new OperationShutdownError();
|
|
99
|
+
for (const controller of this.controllers) {
|
|
100
|
+
if (!controller.signal.aborted)
|
|
101
|
+
controller.abort(reason);
|
|
102
|
+
}
|
|
103
|
+
if (this.completions.size === 0)
|
|
104
|
+
return;
|
|
105
|
+
let timer;
|
|
106
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
107
|
+
timer = setTimeout(() => reject(new OperationDrainTimeoutError(this.shutdownDrainTimeoutMs)), this.shutdownDrainTimeoutMs);
|
|
108
|
+
});
|
|
54
109
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
110
|
+
await Promise.race([
|
|
111
|
+
Promise.allSettled([...this.completions]).then(() => undefined),
|
|
112
|
+
timeout,
|
|
113
|
+
]);
|
|
60
114
|
}
|
|
61
|
-
|
|
62
|
-
|
|
115
|
+
finally {
|
|
116
|
+
if (timer)
|
|
117
|
+
clearTimeout(timer);
|
|
63
118
|
}
|
|
64
119
|
}
|
|
65
|
-
return markdown;
|
|
66
120
|
}
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
121
|
+
export class OperationTimeoutError extends Error {
|
|
122
|
+
constructor(timeoutMs) {
|
|
123
|
+
super(`MCP operation exceeded its ${timeoutMs} ms runtime budget`);
|
|
124
|
+
this.code = 'timeout';
|
|
125
|
+
this.name = 'OperationTimeoutError';
|
|
126
|
+
this.hint = 'Retry with fewer targets/items or increase the server operation timeout.';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export class OperationBusyError extends Error {
|
|
130
|
+
constructor(active, capacity) {
|
|
131
|
+
super(`MCP server is busy; retry later (${active} of ${capacity} expensive-operation slots are active)`);
|
|
132
|
+
this.active = active;
|
|
133
|
+
this.capacity = capacity;
|
|
134
|
+
this.code = 'busy';
|
|
135
|
+
this.retryable = true;
|
|
136
|
+
this.name = 'OperationBusyError';
|
|
137
|
+
this.hint =
|
|
138
|
+
'Retry after one active operation completes; reducing target or preview count may help.';
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export class OperationClosingError extends Error {
|
|
142
|
+
constructor() {
|
|
143
|
+
super('MCP server is closing and is not accepting new operations');
|
|
144
|
+
this.code = 'server-closing';
|
|
145
|
+
this.hint = 'Reconnect to a running DocBlocks MCP server before retrying.';
|
|
146
|
+
this.retryable = true;
|
|
147
|
+
this.name = 'OperationClosingError';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export class OperationShutdownError extends Error {
|
|
151
|
+
constructor() {
|
|
152
|
+
super('MCP operation was cancelled because the server is shutting down');
|
|
153
|
+
this.code = 'server-shutdown';
|
|
154
|
+
this.hint = 'Reconnect to a running DocBlocks MCP server before retrying.';
|
|
155
|
+
this.retryable = true;
|
|
156
|
+
this.name = 'OperationShutdownError';
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export class OperationDrainTimeoutError extends Error {
|
|
160
|
+
constructor(timeoutMs) {
|
|
161
|
+
super(`MCP operations did not finish cleanup within ${timeoutMs} ms`);
|
|
162
|
+
this.code = 'shutdown-drain-timeout';
|
|
163
|
+
this.hint = 'Wait for active converter cleanup, then close the server again.';
|
|
164
|
+
this.retryable = true;
|
|
165
|
+
this.name = 'OperationDrainTimeoutError';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** Retains every independent failure encountered while closing the server. */
|
|
169
|
+
export class McpServerShutdownError extends Error {
|
|
170
|
+
constructor(errors) {
|
|
171
|
+
super(`MCP server shutdown encountered ${errors.length} failures`);
|
|
172
|
+
this.name = 'McpServerShutdownError';
|
|
173
|
+
this.errors = Object.freeze([...errors]);
|
|
174
|
+
this.cause = this.errors[0];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function createMcpServer(options = {}) {
|
|
178
|
+
const authority = McpFileAuthority.create(options);
|
|
179
|
+
// `createMcpServer` remains synchronous for SDK ergonomics, but root
|
|
180
|
+
// resolution is asynchronous. Attach a handler immediately so an invalid
|
|
181
|
+
// startup grant cannot become an unhandled rejection before connect(), then
|
|
182
|
+
// make transport admission wait for every startup prerequisite.
|
|
183
|
+
void authority.catch(() => undefined);
|
|
184
|
+
const operations = new OperationGuard(options.maxConcurrentOperations ?? DEFAULT_MAX_CONCURRENT_OPERATIONS, options.operationTimeoutMs ?? DEFAULT_OPERATION_TIMEOUT_MS, options.shutdownDrainTimeoutMs ?? DEFAULT_SHUTDOWN_DRAIN_TIMEOUT_MS);
|
|
185
|
+
const artifacts = new ArtifactStore(options);
|
|
186
|
+
const server = new McpServer({ name: 'docblocks', version: getPackageVersion() });
|
|
187
|
+
registerAgenticTools(server, {
|
|
188
|
+
authority,
|
|
189
|
+
artifacts,
|
|
190
|
+
runOperation: (signal, work) => operations.run(signal, work),
|
|
71
191
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
format: 'html',
|
|
88
|
-
description: 'Export a markdown document to a self-contained interactive HTML page with an embedded player. Accepts raw markdown text or a file path.',
|
|
89
|
-
},
|
|
90
|
-
];
|
|
91
|
-
for (const { format, description } of EXPORT_FORMATS) {
|
|
92
|
-
server.tool(`export_markdown_to_${format}`, description, {
|
|
93
|
-
markdown: z
|
|
94
|
-
.string()
|
|
95
|
-
.describe('Raw markdown text or path to a .md/.zip/.dbk file or folder'),
|
|
96
|
-
outputPath: z.string().describe(`Output .${format} file path`),
|
|
97
|
-
theme: z.string().optional().describe('Visual theme ID (use list_themes to see options)'),
|
|
98
|
-
transform: z
|
|
99
|
-
.string()
|
|
100
|
-
.optional()
|
|
101
|
-
.describe('Transform style to apply before export (use list_transform_styles to see options)'),
|
|
102
|
-
}, async ({ markdown, outputPath, theme, transform }) => {
|
|
103
|
-
const { filePath, isTemp } = await resolveMarkdownInput(markdown);
|
|
192
|
+
registerDiscoveryTools(server);
|
|
193
|
+
registerAuthoringPrompts(server);
|
|
194
|
+
const connectServer = server.connect.bind(server);
|
|
195
|
+
server.connect = async (transport) => {
|
|
196
|
+
await Promise.all([authority, artifacts.ready()]);
|
|
197
|
+
await connectServer(transport);
|
|
198
|
+
};
|
|
199
|
+
const closeServer = server.close.bind(server);
|
|
200
|
+
let closePromise = null;
|
|
201
|
+
const beginShutdown = (closeTransport) => {
|
|
202
|
+
if (closePromise)
|
|
203
|
+
return closePromise;
|
|
204
|
+
closePromise = (async () => {
|
|
205
|
+
const failures = [];
|
|
104
206
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (file.path !== resolvedOutput) {
|
|
117
|
-
await rename(file.path, resolvedOutput);
|
|
118
|
-
file.path = resolvedOutput;
|
|
207
|
+
await operations.shutdown();
|
|
208
|
+
}
|
|
209
|
+
catch (caught) {
|
|
210
|
+
failures.push(caught);
|
|
211
|
+
}
|
|
212
|
+
if (closeTransport) {
|
|
213
|
+
try {
|
|
214
|
+
await closeServer();
|
|
215
|
+
}
|
|
216
|
+
catch (caught) {
|
|
217
|
+
failures.push(caught);
|
|
119
218
|
}
|
|
120
|
-
return {
|
|
121
|
-
content: [
|
|
122
|
-
{
|
|
123
|
-
type: 'text',
|
|
124
|
-
text: JSON.stringify({
|
|
125
|
-
outputPath: file.path,
|
|
126
|
-
fileSize: file.size,
|
|
127
|
-
format,
|
|
128
|
-
}),
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
};
|
|
132
219
|
}
|
|
133
|
-
|
|
134
|
-
await
|
|
220
|
+
try {
|
|
221
|
+
await artifacts.dispose();
|
|
135
222
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
server.tool('export_markdown_to_video', 'Render a markdown document as an MP4 video with narration-synced animations. Requires ffmpeg and Playwright. Accepts raw markdown text or a file path.', {
|
|
139
|
-
markdown: z.string().describe('Raw markdown text or path to a .md/.zip/.dbk file or folder'),
|
|
140
|
-
outputPath: z.string().describe('Output .mp4 file path'),
|
|
141
|
-
fps: z.number().min(1).max(120).optional().describe('Frames per second (default: 30)'),
|
|
142
|
-
quality: z
|
|
143
|
-
.enum(['draft', 'normal', 'high'])
|
|
144
|
-
.optional()
|
|
145
|
-
.describe('Encoding quality (default: normal)'),
|
|
146
|
-
orientation: z
|
|
147
|
-
.enum(['landscape', 'portrait'])
|
|
148
|
-
.optional()
|
|
149
|
-
.describe('Video orientation (default: landscape)'),
|
|
150
|
-
captions: z
|
|
151
|
-
.enum(['off', 'standard', 'social'])
|
|
152
|
-
.optional()
|
|
153
|
-
.describe('Caption style (default: off)'),
|
|
154
|
-
width: z.number().optional().describe('Override video width in pixels'),
|
|
155
|
-
height: z.number().optional().describe('Override video height in pixels'),
|
|
156
|
-
}, async ({ markdown, outputPath, fps, quality, orientation, captions, width, height }) => {
|
|
157
|
-
const { filePath, isTemp } = await resolveMarkdownInput(markdown);
|
|
158
|
-
try {
|
|
159
|
-
const { runVideo } = await import('../commands/video.js');
|
|
160
|
-
const result = await runVideo(filePath, {
|
|
161
|
-
output: outputPath,
|
|
162
|
-
fps,
|
|
163
|
-
quality,
|
|
164
|
-
orientation,
|
|
165
|
-
captions: captions,
|
|
166
|
-
width,
|
|
167
|
-
height,
|
|
168
|
-
});
|
|
169
|
-
return {
|
|
170
|
-
content: [
|
|
171
|
-
{
|
|
172
|
-
type: 'text',
|
|
173
|
-
text: JSON.stringify({
|
|
174
|
-
outputPath: result.outputPath,
|
|
175
|
-
duration: result.duration,
|
|
176
|
-
frameCount: result.frameCount,
|
|
177
|
-
}),
|
|
178
|
-
},
|
|
179
|
-
],
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
await cleanupTemp(filePath, isTemp);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
// ── Reverse Conversion Tools ─────────────────────────────────────
|
|
187
|
-
const REVERSE_FORMATS = [
|
|
188
|
-
{
|
|
189
|
-
ext: 'docx',
|
|
190
|
-
description: 'Convert a Microsoft Word (.docx) file to Markdown. Preserves headings, paragraphs, emphasis, lists, and tables on a best-effort basis.',
|
|
191
|
-
loader: async () => (await import('../converters/docx-to-md.js')).docxToMarkdown,
|
|
192
|
-
},
|
|
193
|
-
{
|
|
194
|
-
ext: 'pptx',
|
|
195
|
-
description: 'Convert a PowerPoint (.pptx) file to Markdown. Each slide becomes a section; the first paragraph on each slide is treated as the slide title.',
|
|
196
|
-
loader: async () => (await import('../converters/pptx-to-md.js')).pptxToMarkdown,
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
ext: 'pdf',
|
|
200
|
-
description: 'Convert a PDF file to Markdown. Extracts text from each page; formatting and layout are not preserved. Each page becomes a section.',
|
|
201
|
-
loader: async () => (await import('../converters/pdf-to-md.js')).pdfToMarkdown,
|
|
202
|
-
},
|
|
203
|
-
];
|
|
204
|
-
for (const { ext, description, loader } of REVERSE_FORMATS) {
|
|
205
|
-
server.tool(`convert_${ext}_to_markdown`, description, {
|
|
206
|
-
inputPath: z.string().describe(`Path to the source .${ext} file`),
|
|
207
|
-
outputPath: z
|
|
208
|
-
.string()
|
|
209
|
-
.optional()
|
|
210
|
-
.describe('If provided, write the resulting markdown to this file path'),
|
|
211
|
-
}, async ({ inputPath, outputPath }) => {
|
|
212
|
-
const convert = await loader();
|
|
213
|
-
const resolvedInput = resolve(inputPath);
|
|
214
|
-
const markdown = await convert(resolvedInput);
|
|
215
|
-
if (outputPath) {
|
|
216
|
-
await writeFile(resolve(outputPath), markdown, 'utf-8');
|
|
223
|
+
catch (caught) {
|
|
224
|
+
failures.push(caught);
|
|
217
225
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
text: markdown,
|
|
223
|
-
},
|
|
224
|
-
],
|
|
225
|
-
};
|
|
226
|
+
throwShutdownFailures(failures);
|
|
227
|
+
})().catch((caught) => {
|
|
228
|
+
closePromise = null;
|
|
229
|
+
throw caught;
|
|
226
230
|
});
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const { markdownToDoc } = await import('@bendyline/squisq/doc');
|
|
236
|
-
const markdownDoc = parseMarkdown(content);
|
|
237
|
-
const doc = markdownToDoc(markdownDoc);
|
|
238
|
-
// Extract content elements
|
|
239
|
-
const extracted = extractContent(content);
|
|
240
|
-
// Compute structure stats
|
|
241
|
-
const stats = {
|
|
242
|
-
blockCount: doc.blocks?.length ?? 0,
|
|
243
|
-
headingCount: 0,
|
|
244
|
-
paragraphCount: 0,
|
|
245
|
-
wordCount: content.split(/\s+/).filter(Boolean).length,
|
|
246
|
-
characterCount: content.length,
|
|
247
|
-
};
|
|
248
|
-
if (markdownDoc.children) {
|
|
249
|
-
for (const node of markdownDoc.children) {
|
|
250
|
-
if (node.type === 'heading')
|
|
251
|
-
stats.headingCount++;
|
|
252
|
-
if (node.type === 'paragraph')
|
|
253
|
-
stats.paragraphCount++;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
content: [
|
|
258
|
-
{
|
|
259
|
-
type: 'text',
|
|
260
|
-
text: JSON.stringify({ stats, extracted }, null, 2),
|
|
261
|
-
},
|
|
262
|
-
],
|
|
263
|
-
};
|
|
264
|
-
});
|
|
265
|
-
server.tool('restyle_markdown', 'Restyle a markdown document by applying a visual transform — restructures content for a specific presentation style (documentary, magazine, data-driven, narrative, minimal). Returns the transformed markdown text. Use list_transform_styles to see available styles. Accepts raw markdown text or a file path.', {
|
|
266
|
-
markdown: z.string().describe('Raw markdown text or path to a .md file'),
|
|
267
|
-
style: z.string().describe('Transform style ID (use list_transform_styles to see options)'),
|
|
268
|
-
theme: z
|
|
269
|
-
.string()
|
|
270
|
-
.optional()
|
|
271
|
-
.describe('Visual theme ID to apply (use list_themes to see options)'),
|
|
272
|
-
outputPath: z
|
|
273
|
-
.string()
|
|
274
|
-
.optional()
|
|
275
|
-
.describe('If provided, write the transformed markdown to this file path'),
|
|
276
|
-
}, async ({ markdown, style, theme, outputPath }) => {
|
|
277
|
-
const content = await resolveMarkdownText(markdown);
|
|
278
|
-
const { getTransformStyleIds } = await import('@bendyline/squisq/transform');
|
|
279
|
-
const validStyles = getTransformStyleIds();
|
|
280
|
-
if (!validStyles.includes(style)) {
|
|
281
|
-
return {
|
|
282
|
-
content: [
|
|
283
|
-
{
|
|
284
|
-
type: 'text',
|
|
285
|
-
text: `Unknown transform style "${style}". Available: ${validStyles.join(', ')}`,
|
|
286
|
-
},
|
|
287
|
-
],
|
|
288
|
-
isError: true,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
const { parseMarkdown, stringifyMarkdown } = await import('@bendyline/squisq/markdown');
|
|
292
|
-
const { MemoryContentContainer } = await import('@bendyline/squisq/storage');
|
|
293
|
-
const { applyTransformToMarkdown } = await import('../commands/convert.js');
|
|
294
|
-
const markdownDoc = parseMarkdown(content);
|
|
295
|
-
const container = new MemoryContentContainer();
|
|
296
|
-
const transformedMarkdownDoc = await applyTransformToMarkdown(markdownDoc, container, style, theme);
|
|
297
|
-
const transformedText = stringifyMarkdown(transformedMarkdownDoc);
|
|
298
|
-
if (outputPath) {
|
|
299
|
-
await writeFile(resolve(outputPath), transformedText, 'utf-8');
|
|
300
|
-
}
|
|
301
|
-
return {
|
|
302
|
-
content: [
|
|
303
|
-
{
|
|
304
|
-
type: 'text',
|
|
305
|
-
text: transformedText,
|
|
306
|
-
},
|
|
307
|
-
],
|
|
308
|
-
};
|
|
309
|
-
});
|
|
310
|
-
// ── Discovery Tools ──────────────────────────────────────────────
|
|
311
|
-
server.tool('list_themes', 'List all available visual themes (e.g., documentary, cinematic, bold) with descriptions. Use to choose a theme before exporting.', {}, async () => {
|
|
312
|
-
const { getThemeSummaries } = await import('@bendyline/squisq/schemas');
|
|
313
|
-
const themes = getThemeSummaries();
|
|
314
|
-
return {
|
|
315
|
-
content: [
|
|
316
|
-
{
|
|
317
|
-
type: 'text',
|
|
318
|
-
text: JSON.stringify(themes, null, 2),
|
|
319
|
-
},
|
|
320
|
-
],
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
|
-
server.tool('list_transform_styles', 'List all available transform styles (e.g., documentary, magazine, minimal) with descriptions. Use before calling restyle_markdown to see what styles are available.', {}, async () => {
|
|
324
|
-
const { getTransformStyleSummaries } = await import('@bendyline/squisq/transform');
|
|
325
|
-
const styles = getTransformStyleSummaries();
|
|
326
|
-
return {
|
|
327
|
-
content: [
|
|
328
|
-
{
|
|
329
|
-
type: 'text',
|
|
330
|
-
text: JSON.stringify(styles, null, 2),
|
|
331
|
-
},
|
|
332
|
-
],
|
|
333
|
-
};
|
|
334
|
-
});
|
|
335
|
-
server.tool('list_export_formats', 'List all supported export formats with descriptions of what each produces. Use to help choose the right output format.', {}, async () => {
|
|
336
|
-
const formats = {
|
|
337
|
-
input: [
|
|
338
|
-
{ ext: '.md', description: 'Markdown file' },
|
|
339
|
-
{ ext: '.zip/.dbk', description: 'Container archive with embedded media' },
|
|
340
|
-
{ ext: 'folder', description: 'Directory with markdown and media files' },
|
|
341
|
-
{
|
|
342
|
-
ext: '.docx',
|
|
343
|
-
description: 'Microsoft Word document — via convert_docx_to_markdown',
|
|
344
|
-
tool: 'convert_docx_to_markdown',
|
|
345
|
-
},
|
|
346
|
-
{
|
|
347
|
-
ext: '.pptx',
|
|
348
|
-
description: 'PowerPoint presentation — via convert_pptx_to_markdown',
|
|
349
|
-
tool: 'convert_pptx_to_markdown',
|
|
350
|
-
},
|
|
351
|
-
{
|
|
352
|
-
ext: '.pdf',
|
|
353
|
-
description: 'PDF document — via convert_pdf_to_markdown',
|
|
354
|
-
tool: 'convert_pdf_to_markdown',
|
|
355
|
-
},
|
|
356
|
-
],
|
|
357
|
-
output: [
|
|
358
|
-
{
|
|
359
|
-
format: 'docx',
|
|
360
|
-
description: 'Microsoft Word document with professional formatting',
|
|
361
|
-
tool: 'export_markdown_to_docx',
|
|
362
|
-
},
|
|
363
|
-
{
|
|
364
|
-
format: 'pdf',
|
|
365
|
-
description: 'Styled PDF document',
|
|
366
|
-
tool: 'export_markdown_to_pdf',
|
|
367
|
-
},
|
|
368
|
-
{
|
|
369
|
-
format: 'pptx',
|
|
370
|
-
description: 'PowerPoint presentation — each section becomes a slide',
|
|
371
|
-
tool: 'export_markdown_to_pptx',
|
|
372
|
-
},
|
|
373
|
-
{
|
|
374
|
-
format: 'html',
|
|
375
|
-
description: 'Self-contained interactive HTML page with embedded player',
|
|
376
|
-
tool: 'export_markdown_to_html',
|
|
377
|
-
},
|
|
378
|
-
{
|
|
379
|
-
format: 'mp4',
|
|
380
|
-
description: 'Video with narration-synced animations (requires ffmpeg)',
|
|
381
|
-
tool: 'export_markdown_to_video',
|
|
382
|
-
},
|
|
383
|
-
],
|
|
384
|
-
};
|
|
385
|
-
return {
|
|
386
|
-
content: [
|
|
387
|
-
{
|
|
388
|
-
type: 'text',
|
|
389
|
-
text: JSON.stringify(formats, null, 2),
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
};
|
|
393
|
-
});
|
|
394
|
-
// ── Resources ────────────────────────────────────────────────────
|
|
395
|
-
server.resource('formats', 'docblocks://formats', async () => {
|
|
396
|
-
return {
|
|
397
|
-
contents: [
|
|
398
|
-
{
|
|
399
|
-
uri: 'docblocks://formats',
|
|
400
|
-
mimeType: 'application/json',
|
|
401
|
-
text: JSON.stringify({
|
|
402
|
-
description: 'DocBlocks supports converting markdown documents to multiple professional output formats',
|
|
403
|
-
inputFormats: ['.md', '.zip', '.dbk', 'folder', '.docx', '.pptx', '.pdf'],
|
|
404
|
-
outputFormats: ['docx', 'pptx', 'pdf', 'html', 'mp4', 'dbk', 'markdown'],
|
|
405
|
-
}, null, 2),
|
|
406
|
-
},
|
|
407
|
-
],
|
|
408
|
-
};
|
|
409
|
-
});
|
|
410
|
-
// ── Prompts ──────────────────────────────────────────────────────
|
|
411
|
-
server.prompt('create-presentation', 'Create a presentation-ready document from markdown. Guides you through writing content, choosing a theme, applying a transform style, and exporting to PPTX or PDF.', {
|
|
412
|
-
topic: z.string().describe('The topic or subject for the presentation'),
|
|
413
|
-
style: z
|
|
414
|
-
.string()
|
|
415
|
-
.optional()
|
|
416
|
-
.describe('Transform style (documentary, magazine, data-driven, narrative, minimal). If omitted, you will be guided to choose.'),
|
|
417
|
-
}, async ({ topic, style }) => {
|
|
418
|
-
return {
|
|
419
|
-
messages: [
|
|
420
|
-
{
|
|
421
|
-
role: 'user',
|
|
422
|
-
content: {
|
|
423
|
-
type: 'text',
|
|
424
|
-
text: `Create a presentation about: ${topic}
|
|
425
|
-
|
|
426
|
-
Instructions for the AI agent:
|
|
427
|
-
|
|
428
|
-
1. First, call list_themes and list_transform_styles to see available options.
|
|
429
|
-
2. Write well-structured markdown content about the topic. Structure it with clear sections using ## headings — each heading becomes a slide.
|
|
430
|
-
3. Call restyle_markdown with style="${style ?? 'documentary'}" to transform the content for presentation.
|
|
431
|
-
4. Review the restyled markdown and make any adjustments.
|
|
432
|
-
5. Call export_markdown_to_pptx to generate the PowerPoint file.
|
|
433
|
-
|
|
434
|
-
Tips for great presentations:
|
|
435
|
-
- Use ## for slide breaks
|
|
436
|
-
- Keep each section focused on one idea
|
|
437
|
-
- Include statistics and quotes when relevant — they become visual highlights
|
|
438
|
-
- Use bullet lists for key points
|
|
439
|
-
- Add image references with  for visual slides`,
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
};
|
|
444
|
-
});
|
|
445
|
-
server.prompt('create-video', 'Create a video from markdown content. Guides you through writing content optimized for video, choosing a theme, and rendering to MP4.', {
|
|
446
|
-
topic: z.string().describe('The topic or subject for the video'),
|
|
447
|
-
orientation: z
|
|
448
|
-
.enum(['landscape', 'portrait'])
|
|
449
|
-
.optional()
|
|
450
|
-
.describe('Video orientation (default: landscape)'),
|
|
451
|
-
}, async ({ topic, orientation }) => {
|
|
452
|
-
return {
|
|
453
|
-
messages: [
|
|
454
|
-
{
|
|
455
|
-
role: 'user',
|
|
456
|
-
content: {
|
|
457
|
-
type: 'text',
|
|
458
|
-
text: `Create a video about: ${topic}
|
|
459
|
-
|
|
460
|
-
Instructions for the AI agent:
|
|
461
|
-
|
|
462
|
-
1. First, call list_themes to see available visual themes.
|
|
463
|
-
2. Write markdown content optimized for video presentation. The document will be rendered as an animated sequence.
|
|
464
|
-
3. Call analyze_markdown to understand the content structure and choose the best theme.
|
|
465
|
-
4. Call export_markdown_to_video with orientation="${orientation ?? 'landscape'}" to render the video.
|
|
466
|
-
|
|
467
|
-
Tips for great video content:
|
|
468
|
-
- Use clear ## section headings — they create visual transitions
|
|
469
|
-
- Include statistics (numbers with context) — they animate dramatically
|
|
470
|
-
- Add quotes with attribution — they get cinematic treatment
|
|
471
|
-
- Keep paragraphs concise — each maps to a timed visual block
|
|
472
|
-
- The video player auto-times content, so focus on clarity over length`,
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
|
-
],
|
|
476
|
-
};
|
|
477
|
-
});
|
|
478
|
-
server.prompt('create-document', 'Create a professional document from markdown. Guides you through writing content, choosing a theme, and exporting to DOCX or PDF.', {
|
|
479
|
-
topic: z.string().describe('The topic or subject for the document'),
|
|
480
|
-
format: z.enum(['docx', 'pdf']).optional().describe('Output format (default: pdf)'),
|
|
481
|
-
}, async ({ topic, format }) => {
|
|
482
|
-
const outputFormat = format ?? 'pdf';
|
|
483
|
-
const toolName = outputFormat === 'docx' ? 'export_markdown_to_docx' : 'export_markdown_to_pdf';
|
|
484
|
-
return {
|
|
485
|
-
messages: [
|
|
486
|
-
{
|
|
487
|
-
role: 'user',
|
|
488
|
-
content: {
|
|
489
|
-
type: 'text',
|
|
490
|
-
text: `Create a professional document about: ${topic}
|
|
491
|
-
|
|
492
|
-
Instructions for the AI agent:
|
|
493
|
-
|
|
494
|
-
1. First, call list_themes to see available visual themes.
|
|
495
|
-
2. Write well-structured markdown content. Use standard markdown formatting:
|
|
496
|
-
- # for the document title
|
|
497
|
-
- ## for major sections
|
|
498
|
-
- ### for subsections
|
|
499
|
-
- **bold** and *italic* for emphasis
|
|
500
|
-
- > for important quotes or callouts
|
|
501
|
-
- Numbered and bullet lists for organized content
|
|
502
|
-
3. Optionally call restyle_markdown to apply a professional transform.
|
|
503
|
-
4. Call ${toolName} to generate the final document.
|
|
504
|
-
|
|
505
|
-
Tips for professional documents:
|
|
506
|
-
- Start with a clear title and introduction
|
|
507
|
-
- Use consistent heading hierarchy
|
|
508
|
-
- Include data and statistics where appropriate
|
|
509
|
-
- End with a conclusion or summary section`,
|
|
510
|
-
},
|
|
511
|
-
},
|
|
512
|
-
],
|
|
513
|
-
};
|
|
514
|
-
});
|
|
231
|
+
return closePromise;
|
|
232
|
+
};
|
|
233
|
+
const previousOnClose = server.server.onclose;
|
|
234
|
+
server.server.onclose = () => {
|
|
235
|
+
previousOnClose?.();
|
|
236
|
+
void beginShutdown(false).catch(() => undefined);
|
|
237
|
+
};
|
|
238
|
+
server.close = () => beginShutdown(true);
|
|
515
239
|
return server;
|
|
516
240
|
}
|
|
241
|
+
function throwShutdownFailures(failures) {
|
|
242
|
+
if (failures.length === 0)
|
|
243
|
+
return;
|
|
244
|
+
if (failures.length === 1)
|
|
245
|
+
throw failures[0];
|
|
246
|
+
throw new McpServerShutdownError(failures);
|
|
247
|
+
}
|
|
248
|
+
export { MCP_CONVERSION_TARGET_FORMATS, MCP_FORMAT_CAPABILITIES };
|
|
517
249
|
//# sourceMappingURL=server.js.map
|