typst-mcp 1.0.2 → 1.2.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 +49 -87
- package/dist/client/compiler.js +234 -6
- package/dist/client/typst-client.js +306 -52
- package/dist/index.js +97 -5
- package/dist/protocol/edit-diff.js +330 -0
- package/dist/protocol/search-replace.js +97 -0
- package/dist/protocol/yjs-doc.js +31 -38
- package/dist/tools/definitions.js +123 -6
- package/dist/utils/file-mutation-queue.js +30 -0
- package/dist/utils/mime.js +23 -0
- package/dist/utils/truncate.js +114 -0
- package/package.json +3 -1
|
@@ -122,7 +122,7 @@ export const TYPST_MCP_TOOLS = [
|
|
|
122
122
|
},
|
|
123
123
|
{
|
|
124
124
|
name: "typst_read_file",
|
|
125
|
-
description: "Read a file from a https://typst.app project.
|
|
125
|
+
description: "Read a file from a https://typst.app project. Supports text files (.typ, .bib, .toml, etc.) with offset/limit pagination, optional line numbering, and automatic truncation safeguards (2,000 lines or 50KB). Binary image assets (PNG, JPEG, SVG, WebP, GIF, BMP) are returned with native multimodal MCP image content blocks.",
|
|
126
126
|
inputSchema: {
|
|
127
127
|
type: "object",
|
|
128
128
|
properties: {
|
|
@@ -134,6 +134,18 @@ export const TYPST_MCP_TOOLS = [
|
|
|
134
134
|
type: "string",
|
|
135
135
|
description: "File path/name (e.g. 'main.typ') or file ID",
|
|
136
136
|
},
|
|
137
|
+
offset: {
|
|
138
|
+
type: "number",
|
|
139
|
+
description: "Line number to start reading from (1-indexed)",
|
|
140
|
+
},
|
|
141
|
+
limit: {
|
|
142
|
+
type: "number",
|
|
143
|
+
description: "Maximum number of lines to read",
|
|
144
|
+
},
|
|
145
|
+
lineNumbers: {
|
|
146
|
+
type: "boolean",
|
|
147
|
+
description: "Whether to prefix each line with its 1-indexed line number (e.g. '1 | ')",
|
|
148
|
+
},
|
|
137
149
|
},
|
|
138
150
|
required: ["projectId", "filePath"],
|
|
139
151
|
},
|
|
@@ -166,7 +178,7 @@ export const TYPST_MCP_TOOLS = [
|
|
|
166
178
|
},
|
|
167
179
|
{
|
|
168
180
|
name: "typst_edit_file",
|
|
169
|
-
description: "Apply one or more
|
|
181
|
+
description: "Apply one or more surgical text replacements (oldText -> newText) directly to a multiplayer file's Yjs CRDT document in a https://typst.app project. Supports CRLF/LF resilience, UTF-8 BOM preservation, fuzzy matching for Unicode quotes/dashes/whitespace, and returns a unified diff and patch of the changes.",
|
|
170
182
|
inputSchema: {
|
|
171
183
|
type: "object",
|
|
172
184
|
properties: {
|
|
@@ -180,13 +192,13 @@ export const TYPST_MCP_TOOLS = [
|
|
|
180
192
|
},
|
|
181
193
|
edits: {
|
|
182
194
|
type: "array",
|
|
183
|
-
description: "
|
|
195
|
+
description: "One or more targeted non-overlapping replacements. Each oldText must be unique in the original file.",
|
|
184
196
|
items: {
|
|
185
197
|
type: "object",
|
|
186
198
|
properties: {
|
|
187
199
|
oldText: {
|
|
188
200
|
type: "string",
|
|
189
|
-
description: "
|
|
201
|
+
description: "Unique substring to replace (matches exact or normalized quotes/dashes)",
|
|
190
202
|
},
|
|
191
203
|
newText: {
|
|
192
204
|
type: "string",
|
|
@@ -196,12 +208,20 @@ export const TYPST_MCP_TOOLS = [
|
|
|
196
208
|
required: ["oldText", "newText"],
|
|
197
209
|
},
|
|
198
210
|
},
|
|
211
|
+
oldText: {
|
|
212
|
+
type: "string",
|
|
213
|
+
description: "Optional shorthand for a single replacement (used if edits is omitted)",
|
|
214
|
+
},
|
|
215
|
+
newText: {
|
|
216
|
+
type: "string",
|
|
217
|
+
description: "Optional replacement text for single replacement shorthand",
|
|
218
|
+
},
|
|
199
219
|
preferWebSocket: {
|
|
200
220
|
type: "boolean",
|
|
201
221
|
description: "Push real-time TextUpdate over /editing/v3 WebSocket in addition to REST persistence (default: true)",
|
|
202
222
|
},
|
|
203
223
|
},
|
|
204
|
-
required: ["projectId", "filePath"
|
|
224
|
+
required: ["projectId", "filePath"],
|
|
205
225
|
},
|
|
206
226
|
},
|
|
207
227
|
{
|
|
@@ -276,7 +296,7 @@ export const TYPST_MCP_TOOLS = [
|
|
|
276
296
|
},
|
|
277
297
|
{
|
|
278
298
|
name: "typst_get_diagnostics",
|
|
279
|
-
description: "
|
|
299
|
+
description: "Retrieve compiler diagnostics (errors, warnings, line/column numbers, and hints) for a https://typst.app project. Supports both local Typst CLI compilation (fastest) and Typst Web cloud diagnostics (zero-binary fallback using typst.app cloud compiler).",
|
|
280
300
|
inputSchema: {
|
|
281
301
|
type: "object",
|
|
282
302
|
properties: {
|
|
@@ -288,6 +308,11 @@ export const TYPST_MCP_TOOLS = [
|
|
|
288
308
|
type: "string",
|
|
289
309
|
description: "Optional main .typ entry file override (defaults to project previewed file or main.typ)",
|
|
290
310
|
},
|
|
311
|
+
mode: {
|
|
312
|
+
type: "string",
|
|
313
|
+
enum: ["auto", "cli", "web"],
|
|
314
|
+
description: "Diagnostics mode: 'auto' (prefers local Typst CLI if installed, falls back to Typst Web cloud compiler), 'cli' (requires local typst binary), or 'web' (uses Typst Web cloud compiler). Defaults to 'auto'.",
|
|
315
|
+
},
|
|
291
316
|
},
|
|
292
317
|
required: ["projectId"],
|
|
293
318
|
},
|
|
@@ -353,4 +378,96 @@ export const TYPST_MCP_TOOLS = [
|
|
|
353
378
|
required: ["projectId"],
|
|
354
379
|
},
|
|
355
380
|
},
|
|
381
|
+
{
|
|
382
|
+
name: "typst_grep_project",
|
|
383
|
+
description: "Search for text or regular expressions across files in a https://typst.app project. Returns matching lines with file paths, line numbers, and optional surrounding context lines.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
projectId: {
|
|
388
|
+
type: "string",
|
|
389
|
+
description: "The Typst project ID",
|
|
390
|
+
},
|
|
391
|
+
pattern: {
|
|
392
|
+
type: "string",
|
|
393
|
+
description: "Search pattern (regular expression or literal string)",
|
|
394
|
+
},
|
|
395
|
+
path: {
|
|
396
|
+
type: "string",
|
|
397
|
+
description: "Optional directory or file path to restrict search (e.g. 'chapters/' or 'main.typ')",
|
|
398
|
+
},
|
|
399
|
+
glob: {
|
|
400
|
+
type: "string",
|
|
401
|
+
description: "Optional glob pattern to filter files (e.g. '*.typ' or '*.bib')",
|
|
402
|
+
},
|
|
403
|
+
ignoreCase: {
|
|
404
|
+
type: "boolean",
|
|
405
|
+
description: "Case-insensitive search (default: false)",
|
|
406
|
+
},
|
|
407
|
+
literal: {
|
|
408
|
+
type: "boolean",
|
|
409
|
+
description: "Treat pattern as literal string instead of regex (default: false)",
|
|
410
|
+
},
|
|
411
|
+
context: {
|
|
412
|
+
type: "number",
|
|
413
|
+
description: "Number of lines to show before and after each match (default: 0)",
|
|
414
|
+
},
|
|
415
|
+
limit: {
|
|
416
|
+
type: "number",
|
|
417
|
+
description: "Maximum number of matches to return (default: 100)",
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
required: ["projectId", "pattern"],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "typst_find_and_replace",
|
|
425
|
+
description: "Find and replace text or regular expressions across a single file or all files in a https://typst.app project. Supports dry-run preview diffs, regex capture groups, and per-file mutation queue serialization.",
|
|
426
|
+
inputSchema: {
|
|
427
|
+
type: "object",
|
|
428
|
+
properties: {
|
|
429
|
+
projectId: {
|
|
430
|
+
type: "string",
|
|
431
|
+
description: "The Typst project ID",
|
|
432
|
+
},
|
|
433
|
+
pattern: {
|
|
434
|
+
type: "string",
|
|
435
|
+
description: "Search pattern to replace (literal substring or regex)",
|
|
436
|
+
},
|
|
437
|
+
replacement: {
|
|
438
|
+
type: "string",
|
|
439
|
+
description: "Replacement text (supports $1, $2 capture groups if regex is true)",
|
|
440
|
+
},
|
|
441
|
+
filePath: {
|
|
442
|
+
type: "string",
|
|
443
|
+
description: "Optional file path/name (e.g. 'main.typ'). If omitted, searches and replaces across all text files in the project.",
|
|
444
|
+
},
|
|
445
|
+
glob: {
|
|
446
|
+
type: "string",
|
|
447
|
+
description: "Optional glob pattern to filter target files when searching project-wide (e.g. '*.typ')",
|
|
448
|
+
},
|
|
449
|
+
regex: {
|
|
450
|
+
type: "boolean",
|
|
451
|
+
description: "Whether pattern is a regular expression (default: false)",
|
|
452
|
+
},
|
|
453
|
+
ignoreCase: {
|
|
454
|
+
type: "boolean",
|
|
455
|
+
description: "Case-insensitive search (default: false)",
|
|
456
|
+
},
|
|
457
|
+
limit: {
|
|
458
|
+
type: "number",
|
|
459
|
+
description: "Maximum number of replacements per file (default: replace all occurrences)",
|
|
460
|
+
},
|
|
461
|
+
dryRun: {
|
|
462
|
+
type: "boolean",
|
|
463
|
+
description: "If true, returns unified diffs and match counts without saving changes (default: false)",
|
|
464
|
+
},
|
|
465
|
+
preferWebSocket: {
|
|
466
|
+
type: "boolean",
|
|
467
|
+
description: "Push real-time TextUpdate over /editing/v3 WebSocket before REST persistence (default: true)",
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
required: ["projectId", "pattern", "replacement"],
|
|
471
|
+
},
|
|
472
|
+
},
|
|
356
473
|
];
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fileMutationQueues = new Map();
|
|
2
|
+
let registrationQueue = Promise.resolve();
|
|
3
|
+
/**
|
|
4
|
+
* Serialize file mutation operations targeting the same resource key (e.g. `${projectId}:${fileId}`).
|
|
5
|
+
* Operations for different files/projects continue to run concurrently.
|
|
6
|
+
*/
|
|
7
|
+
export async function withFileMutationQueue(key, fn) {
|
|
8
|
+
const registration = registrationQueue.then(async () => {
|
|
9
|
+
const currentQueue = fileMutationQueues.get(key) ?? Promise.resolve();
|
|
10
|
+
let releaseNext;
|
|
11
|
+
const nextQueue = new Promise((resolveQueue) => {
|
|
12
|
+
releaseNext = resolveQueue;
|
|
13
|
+
});
|
|
14
|
+
const chainedQueue = currentQueue.then(() => nextQueue);
|
|
15
|
+
fileMutationQueues.set(key, chainedQueue);
|
|
16
|
+
return { key, currentQueue, chainedQueue, releaseNext };
|
|
17
|
+
});
|
|
18
|
+
registrationQueue = registration.then(() => undefined, () => undefined);
|
|
19
|
+
const { currentQueue, chainedQueue, releaseNext } = await registration;
|
|
20
|
+
await currentQueue;
|
|
21
|
+
try {
|
|
22
|
+
return await fn();
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
releaseNext();
|
|
26
|
+
if (fileMutationQueues.get(key) === chainedQueue) {
|
|
27
|
+
fileMutationQueues.delete(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for detecting supported image MIME types in Typst projects.
|
|
3
|
+
*/
|
|
4
|
+
export function detectSupportedImageMimeType(fileName) {
|
|
5
|
+
const ext = fileName.toLowerCase().split(".").pop();
|
|
6
|
+
switch (ext) {
|
|
7
|
+
case "png":
|
|
8
|
+
return "image/png";
|
|
9
|
+
case "jpg":
|
|
10
|
+
case "jpeg":
|
|
11
|
+
return "image/jpeg";
|
|
12
|
+
case "svg":
|
|
13
|
+
return "image/svg+xml";
|
|
14
|
+
case "webp":
|
|
15
|
+
return "image/webp";
|
|
16
|
+
case "gif":
|
|
17
|
+
return "image/gif";
|
|
18
|
+
case "bmp":
|
|
19
|
+
return "image/bmp";
|
|
20
|
+
default:
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared truncation utilities for tool outputs, ported and adapted from Pi (earendil-works/pi).
|
|
3
|
+
*
|
|
4
|
+
* Truncation is based on two independent limits - whichever is hit first wins:
|
|
5
|
+
* - Line limit (default: 2000 lines)
|
|
6
|
+
* - Byte limit (default: 50KB)
|
|
7
|
+
*
|
|
8
|
+
* Never returns partial lines during normal truncation.
|
|
9
|
+
*/
|
|
10
|
+
export const DEFAULT_MAX_LINES = 2000;
|
|
11
|
+
export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
|
|
12
|
+
function splitLinesForCounting(content) {
|
|
13
|
+
if (content.length === 0) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const lines = content.split("\n");
|
|
17
|
+
if (content.endsWith("\n")) {
|
|
18
|
+
lines.pop();
|
|
19
|
+
}
|
|
20
|
+
return lines;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Format bytes as human-readable size.
|
|
24
|
+
*/
|
|
25
|
+
export function formatSize(bytes) {
|
|
26
|
+
if (bytes < 1024) {
|
|
27
|
+
return `${bytes}B`;
|
|
28
|
+
}
|
|
29
|
+
else if (bytes < 1024 * 1024) {
|
|
30
|
+
return `${(bytes / 1024).toFixed(1)}KB`;
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Truncate content from the head (keep first N lines/bytes).
|
|
38
|
+
* Suitable for file reads where you want to see the beginning.
|
|
39
|
+
*
|
|
40
|
+
* Never returns partial lines. If the first line exceeds the byte limit,
|
|
41
|
+
* returns empty content with firstLineExceedsLimit=true.
|
|
42
|
+
*/
|
|
43
|
+
export function truncateHead(content, options = {}) {
|
|
44
|
+
const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
|
|
45
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
46
|
+
const totalBytes = Buffer.byteLength(content, "utf-8");
|
|
47
|
+
const lines = splitLinesForCounting(content);
|
|
48
|
+
const totalLines = lines.length;
|
|
49
|
+
if (totalLines <= maxLines && totalBytes <= maxBytes) {
|
|
50
|
+
return {
|
|
51
|
+
content,
|
|
52
|
+
truncated: false,
|
|
53
|
+
truncatedBy: null,
|
|
54
|
+
totalLines,
|
|
55
|
+
totalBytes,
|
|
56
|
+
outputLines: totalLines,
|
|
57
|
+
outputBytes: totalBytes,
|
|
58
|
+
lastLinePartial: false,
|
|
59
|
+
firstLineExceedsLimit: false,
|
|
60
|
+
maxLines,
|
|
61
|
+
maxBytes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
// Check if first line alone exceeds byte limit
|
|
65
|
+
const firstLine = lines[0] ?? "";
|
|
66
|
+
const firstLineBytes = Buffer.byteLength(firstLine, "utf-8");
|
|
67
|
+
if (firstLineBytes > maxBytes) {
|
|
68
|
+
return {
|
|
69
|
+
content: "",
|
|
70
|
+
truncated: true,
|
|
71
|
+
truncatedBy: "bytes",
|
|
72
|
+
totalLines,
|
|
73
|
+
totalBytes,
|
|
74
|
+
outputLines: 0,
|
|
75
|
+
outputBytes: 0,
|
|
76
|
+
lastLinePartial: false,
|
|
77
|
+
firstLineExceedsLimit: true,
|
|
78
|
+
maxLines,
|
|
79
|
+
maxBytes,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Collect complete lines that fit
|
|
83
|
+
const outputLinesArr = [];
|
|
84
|
+
let outputBytesCount = 0;
|
|
85
|
+
let truncatedBy = "lines";
|
|
86
|
+
for (let i = 0; i < lines.length && i < maxLines; i++) {
|
|
87
|
+
const line = lines[i];
|
|
88
|
+
const lineBytes = Buffer.byteLength(line, "utf-8") + (i > 0 ? 1 : 0); // +1 for newline
|
|
89
|
+
if (outputBytesCount + lineBytes > maxBytes) {
|
|
90
|
+
truncatedBy = "bytes";
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
outputLinesArr.push(line);
|
|
94
|
+
outputBytesCount += lineBytes;
|
|
95
|
+
}
|
|
96
|
+
if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
|
|
97
|
+
truncatedBy = "lines";
|
|
98
|
+
}
|
|
99
|
+
const outputContent = outputLinesArr.join("\n");
|
|
100
|
+
const finalOutputBytes = Buffer.byteLength(outputContent, "utf-8");
|
|
101
|
+
return {
|
|
102
|
+
content: outputContent,
|
|
103
|
+
truncated: true,
|
|
104
|
+
truncatedBy,
|
|
105
|
+
totalLines,
|
|
106
|
+
totalBytes,
|
|
107
|
+
outputLines: outputLinesArr.length,
|
|
108
|
+
outputBytes: finalOutputBytes,
|
|
109
|
+
lastLinePartial: false,
|
|
110
|
+
firstLineExceedsLimit: false,
|
|
111
|
+
maxLines,
|
|
112
|
+
maxBytes,
|
|
113
|
+
};
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "typst-mcp",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for https://typst.app/ cloud projects, Yjs real-time text editing, asset management, and Typst compiler diagnostics",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -46,12 +46,14 @@
|
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
|
49
|
+
"diff": "^9.0.0",
|
|
49
50
|
"lib0": "^0.2.99",
|
|
50
51
|
"yjs": "^13.6.24",
|
|
51
52
|
"zod": "^3.24.2"
|
|
52
53
|
},
|
|
53
54
|
"devDependencies": {
|
|
54
55
|
"@types/bun": "latest",
|
|
56
|
+
"@types/diff": "^8.0.0",
|
|
55
57
|
"@types/node": "^25.0.3",
|
|
56
58
|
"typescript": "^5.8.2"
|
|
57
59
|
}
|