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 +13 -50
- package/dist/client/compiler.js +234 -6
- package/dist/client/typst-client.js +86 -12
- package/dist/index.js +4 -1
- package/dist/tools/definitions.js +6 -1
- package/package.json +1 -1
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`
|
|
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
|
-
**
|
|
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
|
-
|
|
31
|
+
**Option 2: Always run latest release (bypasses local `npx` cache):**
|
|
32
32
|
|
|
33
|
-
|
|
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` |
|
|
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
|
package/dist/client/compiler.js
CHANGED
|
@@ -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":
|
|
171
|
-
"value":
|
|
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 =
|
|
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(
|
|
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
|
|
575
|
-
const
|
|
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
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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
|
-
|
|
585
|
-
|
|
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),
|
|
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: "
|
|
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