latexmk-mcp 1.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/.gitattributes ADDED
@@ -0,0 +1,5 @@
1
+ # Force ignore build artifacts and lockfiles in stats
2
+ dist/* linguist-vendored
3
+ bun.lock linguist-generated
4
+
5
+ *.ts linguist-language=TypeScript
package/GEMINI.md ADDED
@@ -0,0 +1,60 @@
1
+ # GEMINI.md - Instructional Context for `latexmk-mcp`
2
+
3
+ This file provides context and guidelines for interacting with the `latexmk-mcp` project.
4
+
5
+ ## Project Overview
6
+
7
+ `latexmk-mcp` is a **Model Context Protocol (MCP)** server that exposes the `latexmk` LaTeX build tool as a set of tools for MCP-compatible AI assistants. It allows an AI to compile, check, clean, and inspect LaTeX documents directly through a standardized interface.
8
+
9
+ ### Key Technologies
10
+ - **Runtime:** Node.js (Primary), Bun (Optional for dev)
11
+ - **Language:** TypeScript
12
+ - **Schemas:** Zod
13
+ - **Core SDK:** `@modelcontextprotocol/sdk`
14
+ - **External Tool:** `latexmk` (Must be on `$PATH`)
15
+
16
+ ### Architecture
17
+ The server follows a standard MCP pattern:
18
+ 1. **Tool Definitions:** Located in `src/index.ts`, defining the inputs via Zod schemas and metadata for the MCP `list_tools` request.
19
+ 2. **Handlers:** Asynchronous functions (`handleCompile`, `handleClean`, etc.) that validate input, prepare arguments, and execute `latexmk` as a subprocess.
20
+ 3. **Transport:** Communicates via **stdio**, allowing easy integration with clients like Claude Desktop.
21
+
22
+ ---
23
+
24
+ ## Building and Running
25
+
26
+ ### Prerequisites
27
+ - Node.js 18+ and `npm`.
28
+ - `latexmk` and a TeX distribution (e.g., TeX Live, MiKTeX) must be installed on the system.
29
+
30
+ ### Key Commands
31
+ - **Build:** `npm run build`
32
+ - Compiles TypeScript files from `src/` to `dist/`.
33
+ - **Run (Production):** `npm start`
34
+ - Executes the compiled JavaScript in `dist/index.js`.
35
+ - **Run (Development):** `npm run dev`
36
+ - Runs `src/index.ts` directly using `bun`.
37
+ - **Test:** *TODO: Implement automated tests (e.g., using Vitest or Bun's built-in test runner).*
38
+
39
+ ---
40
+
41
+ ## Development Conventions
42
+
43
+ ### Code Style and Standards
44
+ - **ESM:** The project is a pure ES Module (`"type": "module"` in `package.json`).
45
+ - **TypeScript:** Uses `NodeNext` for module resolution. Avoid using `any`; prefer Zod-inferred types for tool inputs.
46
+ - **Surgical Changes:** When modifying tools, ensure the Zod schema and the handler remain in sync.
47
+
48
+ ### File Structure
49
+ - `src/index.ts`: The main entry point containing all tool definitions and handlers.
50
+ - `dist/`: Compiled JavaScript output (ignored by Git).
51
+ - `tsconfig.json`: Configured for ESM and `NodeNext` resolution.
52
+ - `README.md`: Contains setup and configuration instructions for users.
53
+
54
+ ---
55
+
56
+ ## Future Improvements (Roadmap)
57
+ - [ ] Add a comprehensive test suite with sample `.tex` files.
58
+ - [ ] Implement a file watcher for automatic recompilation.
59
+ - [ ] Add support for more advanced `latexmk` configuration files (`.latexmkrc`).
60
+ - [ ] Improve error parsing for more granular feedback to the user.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 coolport
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # latexmk-mcp
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes the [`latexmk`](https://personal.psu.edu/~jcc8/software/latexmk/) LaTeX build tool as MCP tools — letting any MCP-compatible AI assistant compile, check, clean, and inspect LaTeX documents.
4
+
5
+ ---
6
+
7
+ ## Prerequisites
8
+
9
+ - **Node.js** 18+
10
+ - **latexmk** installed and on `$PATH` (usually ships with TeX Live or MiKTeX)
11
+ - A TeX distribution with your required engines (`pdflatex`, `xelatex`, `lualatex`, …)
12
+
13
+ ```bash
14
+ # Debian/Ubuntu
15
+ sudo apt install latexmk texlive-full
16
+
17
+ # macOS (Homebrew + MacTeX)
18
+ brew install --cask mactex
19
+
20
+ # Arch Linux
21
+ sudo pacman -S texlive-most
22
+ ```
23
+
24
+ ---
25
+
26
+ ## Installation & Build
27
+
28
+ ```bash
29
+ git clone <this-repo>
30
+ cd latexmk-mcp
31
+ npm install
32
+ npm run build # outputs to dist/
33
+ ```
34
+
35
+ ---
36
+
37
+ ## Running
38
+
39
+ ```bash
40
+ # Direct
41
+ node dist/index.js
42
+
43
+ # Via npm
44
+ npm start
45
+
46
+ # During development (no build step)
47
+ npm run dev
48
+ ```
49
+
50
+ The server communicates over **stdio** (standard MCP transport).
51
+
52
+ ---
53
+
54
+ ## Claude Desktop Configuration
55
+
56
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
57
+
58
+ ```json
59
+ {
60
+ "mcpServers": {
61
+ "latexmk": {
62
+ "command": "node",
63
+ "args": ["/absolute/path/to/latexmk-mcp/dist/index.js"]
64
+ }
65
+ }
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Tools
72
+
73
+ ### `latexmk_compile`
74
+ Full compile of a LaTeX document with configurable engine, output format, bibliography processor, and latexmk flags.
75
+
76
+ | Parameter | Type | Default | Description |
77
+ |---|---|---|---|
78
+ | `tex_content` | string | — | Raw LaTeX source (mutually exclusive with `file_path`) |
79
+ | `file_path` | string | — | Path to existing `.tex` file |
80
+ | `output_format` | `pdf\|dvi\|ps\|xdv` | `pdf` | Target format |
81
+ | `engine` | `pdflatex\|xelatex\|lualatex\|latex\|pdftex` | `pdflatex` | TeX engine |
82
+ | `bibtex` | `bibtex\|biber\|none` | `none` | Bibliography processor |
83
+ | `shell_escape` | boolean | `false` | Enable `--shell-escape` |
84
+ | `synctex` | boolean | `false` | Generate SyncTeX data |
85
+ | `extra_args` | string[] | `[]` | Extra latexmk CLI flags |
86
+ | `working_dir` | string | temp dir | Build directory |
87
+
88
+ **Returns:** `success`, `exit_code`, `output_file` path, `errors[]`, `warnings[]`, `working_dir`, `stdout`, `stderr`.
89
+
90
+ ---
91
+
92
+ ### `latexmk_draft_compile`
93
+ Fast single-pass compile (no reruns, no bibliography) — ideal for quick syntax/error checks while editing.
94
+
95
+ | Parameter | Type | Default | Description |
96
+ |---|---|---|---|
97
+ | `tex_content` | string | — | Raw LaTeX source |
98
+ | `file_path` | string | — | Path to `.tex` file |
99
+ | `engine` | string | `pdflatex` | TeX engine |
100
+ | `working_dir` | string | temp dir | Build directory |
101
+
102
+ **Returns:** `success`, `errors[]`, `warnings[]`, `stdout`, `stderr`.
103
+
104
+ ---
105
+
106
+ ### `latexmk_clean`
107
+ Remove build artifacts using `latexmk -c` (auxiliaries only) or `latexmk -C` (auxiliaries + output files).
108
+
109
+ | Parameter | Type | Default | Description |
110
+ |---|---|---|---|
111
+ | `working_dir` | string | **required** | Directory to clean |
112
+ | `job_name` | string | — | Clean a specific job only |
113
+ | `clean_all` | boolean | `false` | `-C` instead of `-c` |
114
+
115
+ ---
116
+
117
+ ### `latexmk_check`
118
+ Detect whether `latexmk` is installed and which TeX engines are available on the system.
119
+
120
+ **Returns:** `latexmk_available`, `latexmk_version`, `latexmk_path`, `engines_available` map.
121
+
122
+ ---
123
+
124
+ ### `latexmk_list_dependencies`
125
+ List all file dependencies of a document (included `.tex` files, `.bib` files, packages, images…) via `latexmk -deps`.
126
+
127
+ | Parameter | Type | Description |
128
+ |---|---|---|
129
+ | `tex_content` | string | Raw LaTeX source |
130
+ | `file_path` | string | Path to `.tex` file |
131
+ | `working_dir` | string | Working directory |
132
+
133
+ **Returns:** `dependencies[]` (deduplicated list of file paths).
134
+
135
+ ---
136
+
137
+ ## Development
138
+
139
+ ```bash
140
+ npm run dev # run with tsx (no build needed)
141
+ npm run build # compile TypeScript → dist/
142
+ npm start # run compiled output
143
+ ```
144
+
145
+ ---
146
+
147
+ ## License
148
+
149
+ MIT
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "latexmk-mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server for the latexmk LaTeX build tool",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "latexmk-mcp": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc",
12
+ "start": "node dist/index.js",
13
+ "dev": "bun run src/index.ts",
14
+ "prepublishOnly": "bun run build"
15
+ },
16
+ "keywords": ["mcp", "latex", "latexmk", "tex"],
17
+ "author": "",
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "@modelcontextprotocol/sdk": "^1.0.0",
21
+ "zod": "^3.23.0"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "^22.0.0",
25
+ "typescript": "^5.0.0"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,671 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import {
6
+ CallToolRequestSchema,
7
+ ListToolsRequestSchema,
8
+ Tool,
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { execFile } from "child_process";
11
+ import { promisify } from "util";
12
+ import { z } from "zod";
13
+ import * as fs from "fs/promises";
14
+ import * as path from "path";
15
+ import * as os from "os";
16
+
17
+ const execFileAsync = promisify(execFile);
18
+
19
+ const CompileSchema = z.object({
20
+ tex_content: z.string().optional().describe("LaTeX source content (if not providing file_path)"),
21
+ file_path: z.string().optional().describe("Absolute path to an existing .tex file"),
22
+ output_format: z
23
+ .enum(["pdf", "dvi", "ps", "xdv"])
24
+ .default("pdf")
25
+ .describe("Target output format"),
26
+ engine: z
27
+ .enum(["pdflatex", "xelatex", "lualatex", "latex", "pdftex"])
28
+ .default("pdflatex")
29
+ .describe("TeX engine to use"),
30
+ bibtex: z
31
+ .enum(["bibtex", "biber", "none"])
32
+ .default("none")
33
+ .describe("Bibliography processor"),
34
+ shell_escape: z.boolean().default(false).describe("Enable shell-escape (--shell-escape)"),
35
+ synctex: z.boolean().default(false).describe("Generate SyncTeX data"),
36
+ extra_args: z.array(z.string()).default([]).describe("Extra latexmk CLI arguments"),
37
+ working_dir: z.string().optional().describe("Working directory (defaults to system temp)"),
38
+ });
39
+
40
+ type CompileOptions = z.infer<typeof CompileSchema>;
41
+
42
+ const CleanSchema = z.object({
43
+ working_dir: z.string().describe("Directory containing the LaTeX build artifacts to clean"),
44
+ job_name: z.string().optional().describe("Specific job name (base filename without extension)"),
45
+ clean_all: z.boolean().default(false).describe("Use -C (remove output files too) instead of -c"),
46
+ });
47
+
48
+ type CleanOptions = z.infer<typeof CleanSchema>;
49
+
50
+ const PreviewSchema = z.object({
51
+ tex_content: z.string().optional().describe("LaTeX source content"),
52
+ file_path: z.string().optional().describe("Absolute path to an existing .tex file"),
53
+ engine: z
54
+ .enum(["pdflatex", "xelatex", "lualatex", "latex"])
55
+ .default("pdflatex")
56
+ .describe("TeX engine"),
57
+ working_dir: z.string().optional().describe("Working directory"),
58
+ });
59
+
60
+ type PreviewOptions = z.infer<typeof PreviewSchema>;
61
+
62
+ const CheckSchema = z.object({
63
+ working_dir: z.string().optional().describe("Directory to check for latexmk availability"),
64
+ });
65
+
66
+ const ListDependenciesSchema = z.object({
67
+ tex_content: z.string().optional().describe("LaTeX source content"),
68
+ file_path: z.string().optional().describe("Absolute path to an existing .tex file"),
69
+ working_dir: z.string().optional().describe("Working directory"),
70
+ });
71
+
72
+ type ListDependenciesOptions = z.infer<typeof ListDependenciesSchema>;
73
+
74
+ // Helpers
75
+
76
+ async function createTempDir(): Promise<string> {
77
+ return fs.mkdtemp(path.join(os.tmpdir(), "latexmk-mcp-"));
78
+ }
79
+
80
+ async function writeTex(content: string, dir: string, name = "document"): Promise<string> {
81
+ const filePath = path.join(dir, `${name}.tex`);
82
+ await fs.writeFile(filePath, content, "utf-8");
83
+ return filePath;
84
+ }
85
+
86
+ async function readFileIfExists(filePath: string): Promise<string | null> {
87
+ try {
88
+ return await fs.readFile(filePath, "utf-8");
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function buildLatexmkArgs(opts: {
95
+ outputFormat: string;
96
+ engine: string;
97
+ bibtex: string;
98
+ shellEscape: boolean;
99
+ synctex: boolean;
100
+ extraArgs: string[];
101
+ jobName?: string;
102
+ outputDir?: string;
103
+ }): string[] {
104
+ const args: string[] = ["-interaction=nonstopmode", "-halt-on-error", "-f"];
105
+
106
+ // Output format
107
+ switch (opts.outputFormat) {
108
+ case "pdf":
109
+ args.push("-pdf");
110
+ break;
111
+ case "dvi":
112
+ args.push("-dvi");
113
+ break;
114
+ case "ps":
115
+ args.push("-ps");
116
+ break;
117
+ case "xdv":
118
+ args.push("-xdv");
119
+ break;
120
+ }
121
+
122
+ // Engine override
123
+ switch (opts.engine) {
124
+ case "xelatex":
125
+ args.push("-xelatex");
126
+ break;
127
+ case "lualatex":
128
+ args.push("-lualatex");
129
+ break;
130
+ case "pdftex":
131
+ case "pdflatex":
132
+ if (opts.outputFormat === "pdf") args.push("-pdflatex");
133
+ break;
134
+ }
135
+
136
+ // Bibliography
137
+ if (opts.bibtex === "bibtex") args.push("-bibtex");
138
+ else if (opts.bibtex === "biber") args.push('-bibtex', '-e', '$biber=q/biber/');
139
+ else args.push("-bibtex-");
140
+
141
+ if (opts.shellEscape) args.push("-shell-escape");
142
+ if (opts.synctex) args.push("-synctex=1");
143
+
144
+ if (opts.outputDir) args.push(`-outdir=${opts.outputDir}`);
145
+ if (opts.jobName) args.push(`-jobname=${opts.jobName}`);
146
+
147
+ return [...args, ...opts.extraArgs];
148
+ }
149
+
150
+ function parseLatexLog(log: string): {
151
+ errors: string[];
152
+ warnings: string[];
153
+ info: string[];
154
+ } {
155
+ const lines = log.split("\n");
156
+ const errors: string[] = [];
157
+ const warnings: string[] = [];
158
+ const info: string[] = [];
159
+
160
+ for (let i = 0; i < lines.length; i++) {
161
+ const line = lines[i]?.trim();
162
+ if (!line) continue;
163
+
164
+ if (/^!(?: LaTeX| Package| Class)? Error/i.test(line)) {
165
+ // Collect multi-line error context
166
+ const ctx = [line];
167
+ while (i + 1 < lines.length && lines[i + 1]?.startsWith(" ")) {
168
+ ctx.push(lines[++i]!.trim());
169
+ }
170
+ errors.push(ctx.join(" "));
171
+ } else if (/^(LaTeX|Package|Class) Warning/i.test(line)) {
172
+ warnings.push(line);
173
+ } else if (line.startsWith("Latexmk:")) {
174
+ info.push(line);
175
+ }
176
+ }
177
+
178
+ return { errors, warnings, info };
179
+ }
180
+
181
+ // Tool Handlers
182
+
183
+ async function handleCompile(rawArgs: unknown) {
184
+ const args: CompileOptions = CompileSchema.parse(rawArgs);
185
+
186
+ if (!args.tex_content && !args.file_path) {
187
+ throw new Error("Either tex_content or file_path must be provided.");
188
+ }
189
+
190
+ const ownDir = !args.working_dir && !args.file_path;
191
+ const workDir = args.working_dir ?? (await createTempDir());
192
+
193
+ let texPath: string;
194
+ let jobName = "document";
195
+
196
+ if (args.file_path) {
197
+ texPath = path.resolve(args.file_path);
198
+ jobName = path.basename(texPath, ".tex");
199
+ } else {
200
+ texPath = await writeTex(args.tex_content!, workDir, jobName);
201
+ }
202
+
203
+ const lmkArgs = buildLatexmkArgs({
204
+ outputFormat: args.output_format,
205
+ engine: args.engine,
206
+ bibtex: args.bibtex,
207
+ shellEscape: args.shell_escape,
208
+ synctex: args.synctex,
209
+ extraArgs: args.extra_args,
210
+ jobName,
211
+ outputDir: workDir,
212
+ });
213
+
214
+ lmkArgs.push(texPath);
215
+
216
+ let stdout = "";
217
+ let stderr = "";
218
+ let exitCode = 0;
219
+
220
+ try {
221
+ const result = await execFileAsync("latexmk", lmkArgs, {
222
+ cwd: workDir,
223
+ maxBuffer: 10 * 1024 * 1024,
224
+ });
225
+ stdout = result.stdout;
226
+ stderr = result.stderr;
227
+ } catch (err: unknown) {
228
+ const e = err as { stdout?: string; stderr?: string; code?: number };
229
+ stdout = e.stdout ?? "";
230
+ stderr = e.stderr ?? "";
231
+ exitCode = e.code ?? 1;
232
+ }
233
+
234
+ const logFile = path.join(workDir, `${jobName}.log`);
235
+ const logContent = (await readFileIfExists(logFile)) ?? "";
236
+ const parsed = parseLatexLog(logContent + "\n" + stdout);
237
+
238
+ const outputExt = args.output_format === "dvi" ? "dvi" : args.output_format === "ps" ? "ps" : args.output_format === "xdv" ? "xdv" : "pdf";
239
+ const outputFile = path.join(workDir, `${jobName}.${outputExt}`);
240
+ let outputExists = false;
241
+ try {
242
+ await fs.access(outputFile);
243
+ outputExists = true;
244
+ } catch { /* noop */ }
245
+
246
+ return {
247
+ success: exitCode === 0 && outputExists,
248
+ exit_code: exitCode,
249
+ output_file: outputExists ? outputFile : null,
250
+ working_dir: workDir,
251
+ errors: parsed.errors,
252
+ warnings: parsed.warnings,
253
+ latexmk_info: parsed.info,
254
+ stdout: stdout.slice(0, 4000),
255
+ stderr: stderr.slice(0, 2000),
256
+ };
257
+ }
258
+
259
+ async function handleClean(rawArgs: unknown) {
260
+ const args: CleanOptions = CleanSchema.parse(rawArgs);
261
+ const flag = args.clean_all ? "-C" : "-c";
262
+
263
+ const lmkArgs = [flag];
264
+ if (args.job_name) lmkArgs.push(`-jobname=${args.job_name}`);
265
+
266
+ let stdout = "";
267
+ let stderr = "";
268
+ let exitCode = 0;
269
+
270
+ try {
271
+ const result = await execFileAsync("latexmk", lmkArgs, {
272
+ cwd: path.resolve(args.working_dir),
273
+ maxBuffer: 5 * 1024 * 1024,
274
+ });
275
+ stdout = result.stdout;
276
+ stderr = result.stderr;
277
+ } catch (err: unknown) {
278
+ const e = err as { stdout?: string; stderr?: string; code?: number };
279
+ stdout = e.stdout ?? "";
280
+ stderr = e.stderr ?? "";
281
+ exitCode = e.code ?? 1;
282
+ }
283
+
284
+ return {
285
+ success: exitCode === 0,
286
+ exit_code: exitCode,
287
+ clean_all: args.clean_all,
288
+ stdout,
289
+ stderr,
290
+ };
291
+ }
292
+
293
+ async function handleDraftCompile(rawArgs: unknown) {
294
+ // Fast single-pass compile to check for errors quickly (no reruns)
295
+ const args: PreviewOptions = PreviewSchema.parse(rawArgs);
296
+
297
+ if (!args.tex_content && !args.file_path) {
298
+ throw new Error("Either tex_content or file_path must be provided.");
299
+ }
300
+
301
+ const workDir = args.working_dir ?? (await createTempDir());
302
+ let texPath: string;
303
+ let jobName = "document";
304
+
305
+ if (args.file_path) {
306
+ texPath = path.resolve(args.file_path);
307
+ jobName = path.basename(texPath, ".tex");
308
+ } else {
309
+ texPath = await writeTex(args.tex_content!, workDir, jobName);
310
+ }
311
+
312
+ const lmkArgs = [
313
+ "-pdf",
314
+ "-interaction=nonstopmode",
315
+ "-f",
316
+ "-bibtex-",
317
+ `-outdir=${workDir}`,
318
+ `-jobname=${jobName}`,
319
+ texPath,
320
+ ];
321
+
322
+ let stdout = "";
323
+ let stderr = "";
324
+ let exitCode = 0;
325
+
326
+ try {
327
+ const result = await execFileAsync("latexmk", lmkArgs, {
328
+ cwd: workDir,
329
+ maxBuffer: 10 * 1024 * 1024,
330
+ });
331
+ stdout = result.stdout;
332
+ stderr = result.stderr;
333
+ } catch (err: unknown) {
334
+ const e = err as { stdout?: string; stderr?: string; code?: number };
335
+ stdout = e.stdout ?? "";
336
+ stderr = e.stderr ?? "";
337
+ exitCode = e.code ?? 1;
338
+ }
339
+
340
+ const logFile = path.join(workDir, `${jobName}.log`);
341
+ const logContent = (await readFileIfExists(logFile)) ?? "";
342
+ const parsed = parseLatexLog(logContent + "\n" + stdout);
343
+
344
+ return {
345
+ success: exitCode === 0,
346
+ exit_code: exitCode,
347
+ working_dir: workDir,
348
+ errors: parsed.errors,
349
+ warnings: parsed.warnings,
350
+ stdout: stdout.slice(0, 4000),
351
+ stderr: stderr.slice(0, 2000),
352
+ };
353
+ }
354
+
355
+ async function handleCheck(_rawArgs: unknown) {
356
+ let version = "";
357
+ let available = false;
358
+ let path_found = "";
359
+
360
+ try {
361
+ const { stdout } = await execFileAsync("latexmk", ["--version"]);
362
+ version = stdout.trim().split("\n")[0] ?? "";
363
+ available = true;
364
+ } catch {
365
+ // latexmk not found
366
+ }
367
+
368
+ try {
369
+ const { stdout } = await execFileAsync("which", ["latexmk"]);
370
+ path_found = stdout.trim();
371
+ } catch {
372
+ // ignore
373
+ }
374
+
375
+ // Check for common TeX engines
376
+ const engines: Record<string, boolean> = {};
377
+ for (const eng of ["pdflatex", "xelatex", "lualatex", "latex"]) {
378
+ try {
379
+ await execFileAsync("which", [eng]);
380
+ engines[eng] = true;
381
+ } catch {
382
+ engines[eng] = false;
383
+ }
384
+ }
385
+
386
+ return {
387
+ latexmk_available: available,
388
+ latexmk_version: version,
389
+ latexmk_path: path_found,
390
+ engines_available: engines,
391
+ };
392
+ }
393
+
394
+ async function handleListDependencies(rawArgs: unknown) {
395
+ const args: ListDependenciesOptions = ListDependenciesSchema.parse(rawArgs);
396
+
397
+ if (!args.tex_content && !args.file_path) {
398
+ throw new Error("Either tex_content or file_path must be provided.");
399
+ }
400
+
401
+ const workDir = args.working_dir ?? (await createTempDir());
402
+ let texPath: string;
403
+ let jobName = "document";
404
+
405
+ if (args.file_path) {
406
+ texPath = path.resolve(args.file_path);
407
+ jobName = path.basename(texPath, ".tex");
408
+ } else {
409
+ texPath = await writeTex(args.tex_content!, workDir, jobName);
410
+ }
411
+
412
+ // Use -deps flag to print dependencies
413
+ const lmkArgs = [
414
+ "-pdf",
415
+ "-deps",
416
+ "-bibtex-",
417
+ "-f",
418
+ `-outdir=${workDir}`,
419
+ `-jobname=${jobName}`,
420
+ texPath,
421
+ ];
422
+
423
+ let stdout = "";
424
+ let exitCode = 0;
425
+
426
+ try {
427
+ const result = await execFileAsync("latexmk", lmkArgs, {
428
+ cwd: workDir,
429
+ maxBuffer: 5 * 1024 * 1024,
430
+ });
431
+ stdout = result.stdout;
432
+ } catch (err: unknown) {
433
+ const e = err as { stdout?: string; code?: number };
434
+ stdout = e.stdout ?? "";
435
+ exitCode = e.code ?? 1;
436
+ }
437
+
438
+ // Parse dependencies from the output
439
+ const deps: string[] = [];
440
+ const depRegex = /^\s{2,}(.+\.(?:tex|bib|sty|cls|clo|def|cfg|fd|enc|tfm|pfb|png|jpg|pdf|eps|svg))\s*\\?$/gim;
441
+ let match;
442
+ while ((match = depRegex.exec(stdout)) !== null) {
443
+ const dep = match[1];
444
+ if (dep) {
445
+ deps.push(dep.trim());
446
+ }
447
+ }
448
+
449
+ return {
450
+ success: exitCode === 0,
451
+ dependencies: [...new Set(deps)],
452
+ working_dir: workDir,
453
+ raw_output: stdout.slice(0, 3000),
454
+ };
455
+ }
456
+
457
+ // Tool Definitions
458
+
459
+ const TOOLS: Tool[] = [
460
+ {
461
+ name: "latexmk_compile",
462
+ description:
463
+ "Compile a LaTeX document using latexmk. Accepts raw LaTeX source or a path to an existing .tex file. Returns compile success/failure, errors, warnings, and the path to the output file.",
464
+ inputSchema: {
465
+ type: "object",
466
+ properties: {
467
+ tex_content: {
468
+ type: "string",
469
+ description: "LaTeX source content (mutually exclusive with file_path)",
470
+ },
471
+ file_path: {
472
+ type: "string",
473
+ description: "Absolute path to an existing .tex file (mutually exclusive with tex_content)",
474
+ },
475
+ output_format: {
476
+ type: "string",
477
+ enum: ["pdf", "dvi", "ps", "xdv"],
478
+ default: "pdf",
479
+ description: "Target output format",
480
+ },
481
+ engine: {
482
+ type: "string",
483
+ enum: ["pdflatex", "xelatex", "lualatex", "latex", "pdftex"],
484
+ default: "pdflatex",
485
+ description: "TeX engine to use",
486
+ },
487
+ bibtex: {
488
+ type: "string",
489
+ enum: ["bibtex", "biber", "none"],
490
+ default: "none",
491
+ description: "Bibliography processor",
492
+ },
493
+ shell_escape: {
494
+ type: "boolean",
495
+ default: false,
496
+ description: "Enable --shell-escape",
497
+ },
498
+ synctex: {
499
+ type: "boolean",
500
+ default: false,
501
+ description: "Generate SyncTeX data",
502
+ },
503
+ extra_args: {
504
+ type: "array",
505
+ items: { type: "string" },
506
+ default: [],
507
+ description: "Extra latexmk CLI arguments to pass through",
508
+ },
509
+ working_dir: {
510
+ type: "string",
511
+ description: "Working directory. Defaults to a fresh temp directory.",
512
+ },
513
+ },
514
+ oneOf: [{ required: ["tex_content"] }, { required: ["file_path"] }],
515
+ },
516
+ },
517
+ {
518
+ name: "latexmk_draft_compile",
519
+ description:
520
+ "Run a fast single-pass draft compile to quickly surface errors without running multiple passes or bibliography. Good for syntax/error checking during editing.",
521
+ inputSchema: {
522
+ type: "object",
523
+ properties: {
524
+ tex_content: {
525
+ type: "string",
526
+ description: "LaTeX source content",
527
+ },
528
+ file_path: {
529
+ type: "string",
530
+ description: "Absolute path to an existing .tex file",
531
+ },
532
+ engine: {
533
+ type: "string",
534
+ enum: ["pdflatex", "xelatex", "lualatex", "latex"],
535
+ default: "pdflatex",
536
+ },
537
+ working_dir: {
538
+ type: "string",
539
+ description: "Working directory",
540
+ },
541
+ },
542
+ oneOf: [{ required: ["tex_content"] }, { required: ["file_path"] }],
543
+ },
544
+ },
545
+ {
546
+ name: "latexmk_clean",
547
+ description:
548
+ "Clean LaTeX build artifacts in a directory using `latexmk -c` (auxiliary files only) or `latexmk -C` (auxiliary + output files).",
549
+ inputSchema: {
550
+ type: "object",
551
+ properties: {
552
+ working_dir: {
553
+ type: "string",
554
+ description: "Directory containing the LaTeX build artifacts",
555
+ },
556
+ job_name: {
557
+ type: "string",
558
+ description: "Specific job name (base filename without extension). Cleans all if omitted.",
559
+ },
560
+ clean_all: {
561
+ type: "boolean",
562
+ default: false,
563
+ description: "If true, uses -C to also remove output files (PDF/DVI/PS). If false, uses -c for auxiliary files only.",
564
+ },
565
+ },
566
+ required: ["working_dir"],
567
+ },
568
+ },
569
+ {
570
+ name: "latexmk_check",
571
+ description:
572
+ "Check whether latexmk is installed and which TeX engines are available on this system.",
573
+ inputSchema: {
574
+ type: "object",
575
+ properties: {
576
+ working_dir: {
577
+ type: "string",
578
+ description: "Optional working directory (not required for this check)",
579
+ },
580
+ },
581
+ },
582
+ },
583
+ {
584
+ name: "latexmk_list_dependencies",
585
+ description:
586
+ "List all file dependencies of a LaTeX document (included .tex files, .bib files, packages, images, etc.) using `latexmk -deps`.",
587
+ inputSchema: {
588
+ type: "object",
589
+ properties: {
590
+ tex_content: {
591
+ type: "string",
592
+ description: "LaTeX source content",
593
+ },
594
+ file_path: {
595
+ type: "string",
596
+ description: "Absolute path to an existing .tex file",
597
+ },
598
+ working_dir: {
599
+ type: "string",
600
+ description: "Working directory",
601
+ },
602
+ },
603
+ oneOf: [{ required: ["tex_content"] }, { required: ["file_path"] }],
604
+ },
605
+ },
606
+ ];
607
+
608
+ // Server
609
+
610
+ const server = new Server(
611
+ { name: "latexmk-mcp", version: "1.0.0" },
612
+ { capabilities: { tools: {} },
613
+ instructions: "Compile, clean, and inspect LaTeX documents using latexmk."
614
+ }
615
+
616
+ );
617
+
618
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
619
+
620
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
621
+ const { name, arguments: args } = request.params;
622
+
623
+ try {
624
+ let result: unknown;
625
+ switch (name) {
626
+ case "latexmk_compile":
627
+ result = await handleCompile(args);
628
+ break;
629
+ case "latexmk_draft_compile":
630
+ result = await handleDraftCompile(args);
631
+ break;
632
+ case "latexmk_clean":
633
+ result = await handleClean(args);
634
+ break;
635
+ case "latexmk_check":
636
+ result = await handleCheck(args);
637
+ break;
638
+ case "latexmk_list_dependencies":
639
+ result = await handleListDependencies(args);
640
+ break;
641
+ default:
642
+ return {
643
+ content: [{ type: "text", text: `Unknown tool: ${name}` }],
644
+ isError: true,
645
+ };
646
+ }
647
+
648
+ return {
649
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
650
+ };
651
+ } catch (err: unknown) {
652
+ const message = err instanceof Error ? err.message : String(err);
653
+ return {
654
+ content: [{ type: "text", text: `Error: ${message}` }],
655
+ isError: true,
656
+ };
657
+ }
658
+ });
659
+
660
+ // Entry
661
+
662
+ async function main() {
663
+ const transport = new StdioServerTransport();
664
+ await server.connect(transport);
665
+ console.error("latexmk MCP server running on stdio");
666
+ }
667
+
668
+ main().catch((err) => {
669
+ console.error("Fatal:", err);
670
+ process.exit(1);
671
+ });
package/src/test.ts ADDED
@@ -0,0 +1 @@
1
+ // Test
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ /* Base environment */
4
+ "lib": ["ESNext"],
5
+ "target": "ESNext",
6
+ "moduleDetection": "force",
7
+ "allowJs": true,
8
+
9
+ /* Building for npm */
10
+ "module": "NodeNext",
11
+ "moduleResolution": "NodeNext",
12
+ "outDir": "dist",
13
+ "sourceMap": true,
14
+ "declaration": true,
15
+ "noEmit": false, // Enabled for npm builds
16
+
17
+ /* Best practices */
18
+ "strict": true,
19
+ "skipLibCheck": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedIndexedAccess": true,
22
+
23
+ /* Project structure */
24
+ "rootDir": "src"
25
+ },
26
+ "include": ["src/**/*"]
27
+ }