typst-mcp 1.1.0 → 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 CHANGED
@@ -18,19 +18,25 @@
18
18
  ### Prerequisites
19
19
 
20
20
  - [**Node.js**](https://nodejs.org/) `>= 18` (for `npx` install) or [**Bun**](https://bun.sh) `>= 1.2` (for local development)
21
- - [**Typst CLI**](https://github.com/typst/typst) (`typst` on `PATH` for `typst_get_diagnostics` and `typst_export_pdf`)
21
+ - [**Typst CLI**](https://github.com/typst/typst) (optional: `typst` on `PATH` for local CLI compilation speed; if not installed, `typst_get_diagnostics` automatically uses Typst Web cloud compiler diagnostics)
22
22
 
23
23
  ### Installation
24
24
 
25
- **Recommended — no clone, no global install:**
25
+ **Option 1: Quick start (no clone, no global install):**
26
26
 
27
27
  ```bash
28
28
  npx -y typst-mcp
29
29
  ```
30
30
 
31
- Runs the published [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) package (Node `>= 18`). Use the same command in MCP config below. `-y` skips the npx install prompt.
31
+ **Option 2: Always run latest release (bypasses local `npx` cache):**
32
32
 
33
- **From source (development):**
33
+ ```bash
34
+ npx -y typst-mcp@latest
35
+ ```
36
+
37
+ Runs the published [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) package (Node `>= 18`). `-y` skips the npx confirmation prompt.
38
+
39
+ **Option 3: From source (development):**
34
40
 
35
41
  ```bash
36
42
  git clone https://github.com/caphefalumi/typst-web-mcp.git
@@ -58,6 +64,8 @@ Add `typst-mcp` to your MCP client configuration (`.pi/agent/mcp-adapter.json`,
58
64
  }
59
65
  ```
60
66
 
67
+ > **Tip:** You can also set `"args": ["-y", "typst-mcp@latest"]` to guarantee your MCP client automatically pulls and runs the newest version on launch.
68
+
61
69
  > **Session Persistence:** Once authenticated via `typst_auth_login` (or environment variables), the `__Http-typst-session` cookie is cached locally at `~/.pi/agent/typst-mcp-session.json` so subsequent tool calls authenticate automatically without re-signing in.
62
70
 
63
71
  ---
@@ -74,21 +82,6 @@ Add `typst-mcp` to your MCP client configuration (`.pi/agent/mcp-adapter.json`,
74
82
 
75
83
  ---
76
84
 
77
- ## Architecture
78
-
79
- ```mermaid
80
- flowchart LR
81
- Agent["AI Agent\n(Pi / Claude / Cursor)"] <-->|"JSON-RPC (stdio)"| MCP["typst-mcp\n(Node / TypeScript)"]
82
- MCP <-->|"GraphQL (/v1/graphql)\n+ X-Typst-Preflight"| API["https://api.typst.app"]
83
- MCP <-->|"REST Yjs & Blobs\n(/v1/files/:pid/:fid/yjs)"| API
84
- MCP <-->|"Binary lib0 WebSocket\n(wss://api.typst.app/editing/v3)"| WS["typst.app Real-Time Sync"]
85
- MCP -->|"Workspace Sync + Compile"| TypstCLI["Typst Compiler\n(Diagnostics & PDF)"]
86
- ```
87
-
88
- See [`docs/PROTOCOL.md`](./docs/PROTOCOL.md) for the complete reverse-engineered protocol specification.
89
-
90
- ---
91
-
92
85
  ## MCP Tools (18 Tools)
93
86
 
94
87
  | Category | Tool Name | Description |
@@ -108,7 +101,7 @@ See [`docs/PROTOCOL.md`](./docs/PROTOCOL.md) for the complete reverse-engineered
108
101
  | **Assets & Tree** | `typst_upload_asset` | Upload or replace binary assets (`PNG`, `JPG`, `SVG`, `PDF`, fonts) from a local path or base64 payload. |
109
102
  | **Assets & Tree** | `typst_rename_file` | Rename or move a file inside a `typst.app` project (`UpdatesFileMutation`). |
110
103
  | **Assets & Tree** | `typst_delete_file` | Delete a file or binary asset from a project (`DeleteFilesMutation`). |
111
- | **Compiler** | `typst_get_diagnostics` | Sync all cloud files and run `typst compile` to return structured errors, warnings, line/column spans, and hints. |
104
+ | **Compiler** | `typst_get_diagnostics` | Retrieve compiler diagnostics (errors, warnings, line/column numbers, and hints). Supports both local Typst CLI (`mode="cli"`) and Typst Web cloud compiler (`mode="web"`), with automatic fallback (`mode="auto"`). |
112
105
  | **Compiler** | `typst_export_pdf` | Sync all cloud text & binary assets, compile the project to PDF, and save to `outputPath`. |
113
106
  | **Preview** | `typst_capture_screenshot` | Capture high-DPI PNG preview screenshots (`mode="pages"` renders compiled PNG pages; `mode="browser"` screenshots the live `typst.app` web UI). |
114
107
 
@@ -143,36 +136,6 @@ Same flow as [Canvas-CLI](https://github.com/caphefalumi/Canvas-CLI):
143
136
 
144
137
  **Trusted publisher (maintainers):** package [`typst-mcp`](https://www.npmjs.com/package/typst-mcp) → Settings → Trusted publishing → GitHub Actions — user/org `caphefalumi`, repo `typst-web-mcp`, workflow file `npm-publish.yml`, environment `npm-publish`, allow **npm publish**.
145
138
 
146
- ### Project Structure
147
-
148
- ```text
149
- typst-web-mcp/
150
- ├── docs/
151
- │ └── PROTOCOL.md # Reverse-engineered typst.app REST, GraphQL, Yjs & WS v3 spec
152
- ├── src/
153
- │ ├── protocol/
154
- │ │ ├── edit-diff.ts # Fuzzy matching, CRLF/BOM resilience, unified diff & patch generator
155
- │ │ ├── ws-codec.ts # Binary lib0 /editing/v3 WebSocket encoder & decoder
156
- │ │ ├── yjs-doc.ts # Yjs CRDT text decoding, replacement & surgical patching
157
- │ │ └── graphql.ts # GraphQL queries, mutations, fragments & types
158
- │ ├── client/
159
- │ │ ├── auth.ts # Direct /v1/auth/signin login, cookie jar & Playwright fallback
160
- │ │ ├── compiler.ts # Typst binary resolver, diagnostic parser, PDF & PNG preview compiler
161
- │ │ ├── errors.ts # Structured TypstLoadError & error formatting helpers
162
- │ │ └── typst-client.ts # TypstCloudClient (REST, GraphQL, WebSocket v3, Compiler facade)
163
- │ ├── tools/
164
- │ │ └── definitions.ts # MCP JSON Schema definitions for all 16 tools
165
- │ ├── utils/
166
- │ │ ├── file-mutation-queue.ts # Serialized mutation mutex queue per file
167
- │ │ ├── mime.ts # Image MIME type detection
168
- │ │ └── truncate.ts # Dual-limit (line & byte) head/tail truncation utilities
169
- │ └── index.ts # MCP stdio server entrypoint (dev); npm bin runs dist/index.js
170
- ├── test/
171
- │ └── typst-mcp.test.ts # 7 test suites (57 assertions) covering codecs, CRDTs & E2E
172
- ├── LICENSE # MIT License
173
- └── package.json
174
- ```
175
-
176
139
  ---
177
140
 
178
141
  ## License
@@ -29,6 +29,52 @@ export function resolveTypstBinary(explicitBin) {
29
29
  }
30
30
  return "typst";
31
31
  }
32
+ export function hasTypstBinary(explicitBin) {
33
+ try {
34
+ const bin = resolveTypstBinary(explicitBin);
35
+ if (bin !== "typst" && existsSync(bin)) {
36
+ return true;
37
+ }
38
+ const proc = spawnSync(bin, ["--version"], {
39
+ stdio: "ignore",
40
+ encoding: "utf8",
41
+ });
42
+ return proc.status === 0 && !proc.error;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ export function resolvePythonBinary() {
49
+ if (process.env.PYTHON_BIN) {
50
+ return process.env.PYTHON_BIN;
51
+ }
52
+ for (const cmd of ["python", "python3", "py"]) {
53
+ try {
54
+ const res = spawnSync(cmd, ["--version"], { stdio: "ignore" });
55
+ if (res.status === 0) {
56
+ return cmd;
57
+ }
58
+ }
59
+ catch { }
60
+ }
61
+ return "python";
62
+ }
63
+ export function offsetToPosition(text, offset) {
64
+ let line = 1;
65
+ let column = 1;
66
+ const target = Math.min(Math.max(0, offset), text.length);
67
+ for (let i = 0; i < target; i++) {
68
+ if (text[i] === "\n") {
69
+ line++;
70
+ column = 1;
71
+ }
72
+ else {
73
+ column++;
74
+ }
75
+ }
76
+ return { line, column };
77
+ }
32
78
  function hasValidPngSignature(bytes) {
33
79
  return (bytes.byteLength >= 8 &&
34
80
  bytes[0] === 0x89 &&
@@ -95,6 +141,9 @@ export function compileWorkspaceDiagnostics(params) {
95
141
  cwd: params.workspaceDir,
96
142
  encoding: "utf8",
97
143
  });
144
+ if (proc.error) {
145
+ throw new TypstLoadError(["TYPST_EXECUTION_FAILED"], `Failed to execute Typst compiler binary '${params.typstBin}': ${proc.error.message}`);
146
+ }
98
147
  const combinedOutput = `${proc.stderr ?? ""}\n${proc.stdout ?? ""}`;
99
148
  const diagnostics = parseShortDiagnostics(combinedOutput).map((d) => ({
100
149
  ...d,
@@ -130,6 +179,9 @@ export function compileWorkspacePdf(params) {
130
179
  cwd: params.workspaceDir,
131
180
  encoding: "utf8",
132
181
  });
182
+ if (proc.error) {
183
+ throw new TypstLoadError(["TYPST_EXECUTION_FAILED"], `Failed to execute Typst compiler binary '${params.typstBin}': ${proc.error.message}`);
184
+ }
133
185
  const combinedOutput = `${proc.stderr ?? ""}\n${proc.stdout ?? ""}`;
134
186
  const diagnostics = parseShortDiagnostics(combinedOutput);
135
187
  if (proc.status !== 0) {
@@ -148,12 +200,169 @@ export function compileWorkspacePdf(params) {
148
200
  diagnostics,
149
201
  };
150
202
  }
203
+ export function captureWebDiagnostics(params) {
204
+ const pythonBin = resolvePythonBinary();
205
+ const timeoutSec = ((params.timeoutMs ?? 25000) / 1000).toFixed(1);
206
+ const pyScript = `
207
+ import os, sys, json, time
208
+ from playwright.sync_api import sync_playwright
209
+
210
+ app_url = os.environ.get("TYPST_APP_URL", "https://typst.app")
211
+ proj_id = os.environ["TYPST_PROJ_ID"]
212
+ raw_cookie = os.environ.get("TYPST_COOKIE", "")
213
+ timeout_sec = float(os.environ.get("TYPST_TIMEOUT_SEC", "25.0"))
214
+
215
+ cookies = []
216
+ for part in raw_cookie.split(";"):
217
+ part = part.strip()
218
+ if "=" in part:
219
+ k, v = part.split("=", 1)
220
+ name = k.strip()
221
+ val = v.strip()
222
+ is_session = "__Http" in name or "session" in name
223
+ cookies.append({
224
+ "name": name,
225
+ "value": val,
226
+ "domain": ".typst.app",
227
+ "path": "/",
228
+ "httpOnly": is_session,
229
+ "secure": True,
230
+ "sameSite": "Strict" if is_session else "Lax",
231
+ })
232
+
233
+ with sync_playwright() as p:
234
+ browser = None
235
+ for channel in ["msedge", "chrome", None]:
236
+ try:
237
+ if channel:
238
+ browser = p.chromium.launch(channel=channel, headless=True)
239
+ else:
240
+ browser = p.chromium.launch(headless=True)
241
+ break
242
+ except Exception:
243
+ continue
244
+
245
+ if browser is None:
246
+ sys.stderr.write("NO_BROWSER_FOUND")
247
+ sys.exit(2)
248
+
249
+ ctx = browser.new_context(viewport={"width": 1920, "height": 1080})
250
+ if cookies:
251
+ ctx.add_cookies(cookies)
252
+
253
+ ctx.add_init_script("""
254
+ window.__typst_diagnostics = null;
255
+ window.__typst_status = 'starting';
256
+ const OrigWorker = window.Worker;
257
+ window.Worker = function(url, opts) {
258
+ const w = new OrigWorker(url, opts);
259
+ w.addEventListener('message', (ev) => {
260
+ try {
261
+ if (ev.data && ev.data.tag === 'notification') {
262
+ if (ev.data.name === 'diagnostics') {
263
+ window.__typst_diagnostics = ev.data.notification?.diagnostics || [];
264
+ } else if (ev.data.name === 'status') {
265
+ window.__typst_status = ev.data.notification?.status || 'idle';
266
+ }
267
+ }
268
+ } catch(e) {}
269
+ });
270
+ return w;
271
+ };
272
+ """)
273
+
274
+ page = ctx.new_page()
275
+ page.goto(f"{app_url}/project/{proj_id}", wait_until="commit", timeout=45000)
276
+
277
+ start_time = time.time()
278
+ diags = None
279
+ while time.time() - start_time < timeout_sec:
280
+ data = page.evaluate("({ diag: window.__typst_diagnostics, status: window.__typst_status, url: window.location.href })")
281
+ if "signin" in data.get("url", ""):
282
+ browser.close()
283
+ sys.stderr.write(f"AUTH_REQUIRED: Redirected to {data.get('url')}")
284
+ sys.exit(3)
285
+ if data.get("diag") is not None:
286
+ diags = data.get("diag")
287
+ if data.get("status") == "idle":
288
+ break
289
+ page.wait_for_timeout(300)
290
+
291
+ browser.close()
292
+ print(json.dumps(diags or []))
293
+ `;
294
+ const proc = spawnSync(pythonBin, ["-c", pyScript], {
295
+ env: {
296
+ ...process.env,
297
+ TYPST_APP_URL: params.appUrl,
298
+ TYPST_PROJ_ID: params.projectId,
299
+ TYPST_COOKIE: params.cookieHeader,
300
+ TYPST_TIMEOUT_SEC: timeoutSec,
301
+ },
302
+ encoding: "utf8",
303
+ timeout: (params.timeoutMs ?? 25000) + 15000,
304
+ });
305
+ if (proc.error || proc.status !== 0) {
306
+ const stderr = (proc.stderr || proc.error?.message || "").trim();
307
+ if (stderr.includes("NO_BROWSER_FOUND")) {
308
+ throw new TypstLoadError(["NO_BROWSER_FOUND"], "No compatible browser (Edge, Chrome, or Chromium) found for Playwright web diagnostics.");
309
+ }
310
+ if (stderr.includes("AUTH_REQUIRED")) {
311
+ throw new TypstLoadError(["UNAUTHENTICATED"], `Typst Web cloud session expired or invalid: ${stderr}`);
312
+ }
313
+ throw new TypstLoadError(["WEB_DIAGNOSTICS_FAILED"], `Failed to capture Typst Web cloud diagnostics: ${stderr || "Ensure python with playwright is installed ('pip install playwright && playwright install')"}`);
314
+ }
315
+ const rawJson = (proc.stdout || "").trim();
316
+ let rawList = [];
317
+ try {
318
+ rawList = JSON.parse(rawJson);
319
+ }
320
+ catch (err) {
321
+ throw new TypstLoadError(["WEB_DIAGNOSTICS_PARSE_FAILED"], `Failed to parse Typst Web diagnostics output: ${err.message}`);
322
+ }
323
+ const mainFileName = params.mainFile ?? "main.typ";
324
+ const diagnostics = rawList.map((raw) => {
325
+ let line = 1;
326
+ let column = 1;
327
+ const filePath = raw.path || mainFileName;
328
+ const fileText = params.fileContents?.[filePath];
329
+ if (fileText && raw.range?.start !== undefined) {
330
+ const pos = offsetToPosition(fileText, raw.range.start);
331
+ line = pos.line;
332
+ column = pos.column;
333
+ }
334
+ return {
335
+ path: filePath,
336
+ line,
337
+ column,
338
+ severity: raw.severity === "error" ? "error" : "warning",
339
+ message: raw.message,
340
+ hints: Array.isArray(raw.hints) ? raw.hints : [],
341
+ package: raw.package ?? null,
342
+ range: raw.range
343
+ ? { start: raw.range.start, end: raw.range.end }
344
+ : undefined,
345
+ };
346
+ });
347
+ const errorCount = diagnostics.filter((d) => d.severity === "error").length;
348
+ const warningCount = diagnostics.filter((d) => d.severity === "warning").length;
349
+ return {
350
+ projectId: params.projectId,
351
+ mainFile: mainFileName,
352
+ mode: "web",
353
+ success: errorCount === 0,
354
+ errorCount,
355
+ warningCount,
356
+ diagnostics,
357
+ };
358
+ }
151
359
  export function captureBrowserPreviewScreenshot(params) {
152
360
  const outputDir = resolveOutputDirectory(params.outputDir, `typst-preview-ui-${params.projectId}-`);
153
361
  mkdirSync(outputDir, { recursive: true });
154
362
  const screenshotPath = join(outputDir, `typst-app-${params.projectId}.png`);
363
+ const pythonBin = resolvePythonBinary();
155
364
  const pyScript = `
156
- import os
365
+ import os, sys
157
366
  from playwright.sync_api import sync_playwright
158
367
 
159
368
  app_url = os.environ["TYPST_PREVIEW_APP_URL"]
@@ -166,16 +375,35 @@ for part in raw_cookie.split(";"):
166
375
  part = part.strip()
167
376
  if "=" in part:
168
377
  k, v = part.split("=", 1)
378
+ name = k.strip()
379
+ val = v.strip()
380
+ is_session = "__Http" in name or "session" in name
169
381
  cookies.append({
170
- "name": k.strip(),
171
- "value": v.strip(),
172
- "domain": "typst.app",
382
+ "name": name,
383
+ "value": val,
384
+ "domain": ".typst.app",
173
385
  "path": "/",
386
+ "httpOnly": is_session,
174
387
  "secure": True,
388
+ "sameSite": "Strict" if is_session else "Lax",
175
389
  })
176
390
 
177
391
  with sync_playwright() as p:
178
- browser = p.chromium.launch(channel="msedge", headless=True)
392
+ browser = None
393
+ for channel in ["msedge", "chrome", None]:
394
+ try:
395
+ if channel:
396
+ browser = p.chromium.launch(channel=channel, headless=True)
397
+ else:
398
+ browser = p.chromium.launch(headless=True)
399
+ break
400
+ except Exception:
401
+ continue
402
+
403
+ if browser is None:
404
+ sys.stderr.write("NO_BROWSER_FOUND")
405
+ sys.exit(2)
406
+
179
407
  ctx = browser.new_context(viewport={"width": 1920, "height": 1080}, device_scale_factor=2)
180
408
  if cookies:
181
409
  ctx.add_cookies(cookies)
@@ -185,7 +413,7 @@ with sync_playwright() as p:
185
413
  page.screenshot(path=out_path, full_page=False)
186
414
  browser.close()
187
415
  `;
188
- const proc = spawnSync("python", ["-c", pyScript], {
416
+ const proc = spawnSync(pythonBin, ["-c", pyScript], {
189
417
  env: {
190
418
  ...process.env,
191
419
  TYPST_PREVIEW_APP_URL: params.appUrl,
@@ -3,8 +3,8 @@ import { dirname, join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { TypstAuthManager } from "./auth.js";
5
5
  import { TypstLoadError, formatUnknownError } from "./errors.js";
6
- import { captureBrowserPreviewScreenshot, captureWorkspacePageScreenshots, compileWorkspaceDiagnostics, compileWorkspacePdf, parseShortDiagnostics, resolveTypstBinary, } from "./compiler.js";
7
- import { CREATE_PROJECT_MUTATION, DASHBOARD_CONTENT_QUERY, DELETE_FILES_MUTATION, DELETE_PROJECT_MUTATION, PROJECT_FILES_QUERY, UPDATE_FILES_MUTATION, } from "../protocol/graphql.js";
6
+ import { captureBrowserPreviewScreenshot, captureWebDiagnostics, captureWorkspacePageScreenshots, compileWorkspaceDiagnostics, compileWorkspacePdf, hasTypstBinary, parseShortDiagnostics, resolveTypstBinary, } from "./compiler.js";
7
+ import { CREATE_PROJECT_MUTATION, DASHBOARD_CONTENT_QUERY, DELETE_FILES_MUTATION, DELETE_PROJECT_MUTATION, EDIT_PROJECT_MUTATION, PROJECT_FILES_QUERY, UPDATE_FILES_MUTATION, } from "../protocol/graphql.js";
8
8
  import { decodeYjsText, editYjsText, replaceYjsText, } from "../protocol/yjs-doc.js";
9
9
  import { decodeMessage, encodeMessage, MessageKind, } from "../protocol/ws-codec.js";
10
10
  import { truncateHead, formatSize, } from "../utils/truncate.js";
@@ -571,18 +571,92 @@ export class TypstCloudClient {
571
571
  static parseShortDiagnostics(stderrText) {
572
572
  return parseShortDiagnostics(stderrText);
573
573
  }
574
- async getDiagnostics(projectId, mainFile) {
575
- const { workspaceDir, mainFileName } = await this.materializeProjectWorkspace(projectId, mainFile);
574
+ async updateProject(projectId, updates) {
575
+ const res = await this.graphql(EDIT_PROJECT_MUTATION, {
576
+ projectId,
577
+ name: updates.name,
578
+ previewed: updates.previewed,
579
+ pinnedVersion: updates.pinnedVersion,
580
+ });
581
+ return res.updateProject;
582
+ }
583
+ async getWebDiagnostics(projectId, mainFile) {
584
+ const project = await this.getProject(projectId);
585
+ let effectiveMainFile = mainFile;
586
+ if (!effectiveMainFile) {
587
+ const previewedFile = project.files.find((f) => f.id === project.previewed);
588
+ effectiveMainFile = previewedFile?.name ?? "main.typ";
589
+ }
590
+ else {
591
+ const requestedFile = project.files.find((f) => f.name === mainFile || f.id === mainFile);
592
+ if (requestedFile && requestedFile.id !== project.previewed) {
593
+ try {
594
+ await this.updateProject(projectId, { previewed: requestedFile.id });
595
+ }
596
+ catch {
597
+ // If permission denied or mutation rejected, proceed anyway
598
+ }
599
+ }
600
+ }
601
+ const fileContents = {};
602
+ for (const f of project.files) {
603
+ if (f.isMultiplayable) {
604
+ try {
605
+ const res = await this.readFile(projectId, f.name);
606
+ if (res.encoding === "utf-8" && typeof res.content === "string") {
607
+ fileContents[f.name] = res.content;
608
+ }
609
+ }
610
+ catch {
611
+ // Ignore read failures for auxiliary files
612
+ }
613
+ }
614
+ }
615
+ return captureWebDiagnostics({
616
+ appUrl: this.auth.appUrl,
617
+ projectId,
618
+ cookieHeader: this.auth.getCookieHeader(),
619
+ mainFile: effectiveMainFile,
620
+ fileContents,
621
+ });
622
+ }
623
+ async getDiagnostics(projectId, options) {
624
+ const opts = typeof options === "string" ? { mainFile: options } : options;
625
+ const mode = opts?.mode ?? "auto";
626
+ const requestedMainFile = opts?.mainFile;
627
+ if (mode === "web") {
628
+ return await this.getWebDiagnostics(projectId, requestedMainFile);
629
+ }
630
+ const typstAvailable = hasTypstBinary(this.typstBin);
631
+ if (!typstAvailable) {
632
+ if (mode === "cli") {
633
+ throw new TypstLoadError(["TYPST_BINARY_NOT_FOUND"], `Typst compiler binary '${this.typstBin}' was not found. Please install the Typst compiler (e.g. 'winget install Typst.Typst' / 'brew install typst') or use mode: 'web' for zero-binary cloud diagnostics.`);
634
+ }
635
+ return await this.getWebDiagnostics(projectId, requestedMainFile);
636
+ }
576
637
  try {
577
- return compileWorkspaceDiagnostics({
578
- typstBin: this.typstBin,
579
- projectId,
580
- workspaceDir,
581
- mainFileName,
582
- });
638
+ const { workspaceDir, mainFileName } = await this.materializeProjectWorkspace(projectId, requestedMainFile);
639
+ try {
640
+ const res = compileWorkspaceDiagnostics({
641
+ typstBin: this.typstBin,
642
+ projectId,
643
+ workspaceDir,
644
+ mainFileName,
645
+ });
646
+ return {
647
+ ...res,
648
+ mode: "cli",
649
+ };
650
+ }
651
+ finally {
652
+ rmSync(workspaceDir, { recursive: true, force: true });
653
+ }
583
654
  }
584
- finally {
585
- rmSync(workspaceDir, { recursive: true, force: true });
655
+ catch (err) {
656
+ if (mode === "auto") {
657
+ return await this.getWebDiagnostics(projectId, requestedMainFile);
658
+ }
659
+ throw err;
586
660
  }
587
661
  }
588
662
  async exportPdf(projectId, options) {
package/dist/index.js CHANGED
@@ -119,7 +119,10 @@ export async function dispatchTypstTool(client, name, args = {}) {
119
119
  case "typst_rename_file":
120
120
  return await client.renameFile(String(args.projectId), String(args.filePath), String(args.newName));
121
121
  case "typst_get_diagnostics":
122
- return await client.getDiagnostics(String(args.projectId), args.mainFile);
122
+ return await client.getDiagnostics(String(args.projectId), {
123
+ mainFile: args.mainFile,
124
+ mode: args.mode,
125
+ });
123
126
  case "typst_export_pdf":
124
127
  return await client.exportPdf(String(args.projectId), {
125
128
  mainFile: args.mainFile,
@@ -296,7 +296,7 @@ export const TYPST_MCP_TOOLS = [
296
296
  },
297
297
  {
298
298
  name: "typst_get_diagnostics",
299
- 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).",
300
300
  inputSchema: {
301
301
  type: "object",
302
302
  properties: {
@@ -308,6 +308,11 @@ export const TYPST_MCP_TOOLS = [
308
308
  type: "string",
309
309
  description: "Optional main .typ entry file override (defaults to project previewed file or main.typ)",
310
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
+ },
311
316
  },
312
317
  required: ["projectId"],
313
318
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typst-mcp",
3
- "version": "1.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",