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.
@@ -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. Decodes Yjs CRDT state (doc.getText('text')) into UTF-8 text for multiplayer files (.typ, .bib, .toml, etc.) or returns base64 for binary assets.",
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 exact surgical text replacements (oldText -> newText) directly to a multiplayer file's Yjs CRDT document in a https://typst.app project.",
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: "Non-overlapping exact text replacements to apply to the Yjs document",
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: "Exact unique substring to replace",
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", "edits"],
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: "Synchronize all files from a https://typst.app project and run the Typst compiler to retrieve structured errors, warnings, line/column numbers, and hints.",
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.2",
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
  }