texflow-mcp 1.0.0__tar.gz
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.
- texflow_mcp-1.0.0/LICENSE +21 -0
- texflow_mcp-1.0.0/MANIFEST.in +4 -0
- texflow_mcp-1.0.0/PKG-INFO +138 -0
- texflow_mcp-1.0.0/README.md +116 -0
- texflow_mcp-1.0.0/pyproject.toml +50 -0
- texflow_mcp-1.0.0/server.py +226 -0
- texflow_mcp-1.0.0/setup.cfg +4 -0
- texflow_mcp-1.0.0/tests/test_compiler.py +99 -0
- texflow_mcp-1.0.0/tests/test_formatters.py +232 -0
- texflow_mcp-1.0.0/tests/test_ingestion.py +219 -0
- texflow_mcp-1.0.0/tests/test_model.py +206 -0
- texflow_mcp-1.0.0/tests/test_queue.py +179 -0
- texflow_mcp-1.0.0/tests/test_serializer.py +380 -0
- texflow_mcp-1.0.0/tests/test_tools.py +400 -0
- texflow_mcp-1.0.0/tests/test_workflow.py +245 -0
- texflow_mcp-1.0.0/texflow/__init__.py +3 -0
- texflow_mcp-1.0.0/texflow/compiler.py +181 -0
- texflow_mcp-1.0.0/texflow/data/class_defaults.json +44 -0
- texflow_mcp-1.0.0/texflow/data/font_map.json +21 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/README.md +58 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/build_reference_data.py +119 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/commands/math.json +105 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/commands/package-mappings.json +73 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/commands/sectioning.json +76 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/errors/patterns.json +113 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/metadata.json +22 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/packages/amsmath.json +89 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/packages/common-packages.json +80 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/symbols/greek.json +149 -0
- texflow_mcp-1.0.0/texflow/data/latex_reference/symbols/math.json +184 -0
- texflow_mcp-1.0.0/texflow/formatters.py +178 -0
- texflow_mcp-1.0.0/texflow/ingestion.py +310 -0
- texflow_mcp-1.0.0/texflow/model.py +337 -0
- texflow_mcp-1.0.0/texflow/serializer.py +452 -0
- texflow_mcp-1.0.0/texflow/tools/__init__.py +1 -0
- texflow_mcp-1.0.0/texflow/tools/document.py +122 -0
- texflow_mcp-1.0.0/texflow/tools/edit.py +239 -0
- texflow_mcp-1.0.0/texflow/tools/layout.py +169 -0
- texflow_mcp-1.0.0/texflow/tools/queue.py +137 -0
- texflow_mcp-1.0.0/texflow/tools/reference.py +399 -0
- texflow_mcp-1.0.0/texflow/tools/render.py +74 -0
- texflow_mcp-1.0.0/texflow/tools/state.py +76 -0
- texflow_mcp-1.0.0/texflow/tools/workflow.py +123 -0
- texflow_mcp-1.0.0/texflow_mcp.egg-info/PKG-INFO +138 -0
- texflow_mcp-1.0.0/texflow_mcp.egg-info/SOURCES.txt +47 -0
- texflow_mcp-1.0.0/texflow_mcp.egg-info/dependency_links.txt +1 -0
- texflow_mcp-1.0.0/texflow_mcp.egg-info/entry_points.txt +2 -0
- texflow_mcp-1.0.0/texflow_mcp.egg-info/requires.txt +2 -0
- texflow_mcp-1.0.0/texflow_mcp.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Aaron Bockelie
|
|
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.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: texflow-mcp
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: LaTeX document compiler with MCP interface
|
|
5
|
+
Author-email: Aaron Bockelie <aaronsb@users.noreply.github.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aaronsb/texflow-mcp
|
|
8
|
+
Project-URL: Repository, https://github.com/aaronsb/texflow-mcp
|
|
9
|
+
Project-URL: Issues, https://github.com/aaronsb/texflow-mcp/issues
|
|
10
|
+
Keywords: mcp,latex,document,typesetting,pdf
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Text Processing :: Markup :: LaTeX
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: fastmcp>=3.0.0
|
|
20
|
+
Requires-Dist: mistune>=3.2.0
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# TeXFlow MCP
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+

|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
A LaTeX document compiler with an [MCP](https://modelcontextprotocol.io) interface. AI agents operate on a structured document model — sections, paragraphs, figures, tables — while TeXFlow handles all LaTeX mechanics: packages, preamble, fonts, and compilation.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install texflow-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Or run without installing:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uvx texflow-mcp
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### System dependencies (optional)
|
|
44
|
+
|
|
45
|
+
TeXFlow compiles documents to PDF using XeLaTeX. Without it, you can still build and export `.tex` files.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Arch
|
|
49
|
+
pacman -S texlive-xetex texlive-fontsrecommended texlive-fontsextra
|
|
50
|
+
|
|
51
|
+
# Debian/Ubuntu
|
|
52
|
+
apt install texlive-xetex texlive-fonts-recommended texlive-fonts-extra
|
|
53
|
+
|
|
54
|
+
# Fedora
|
|
55
|
+
dnf install texlive-xetex texlive-collection-fontsrecommended texlive-collection-fontsextra
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For page preview (PNG), install `poppler-utils` (provides `pdftoppm`).
|
|
59
|
+
|
|
60
|
+
## Configure with Claude Code
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
claude mcp add texflow -- uvx texflow-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
That's it. Restart Claude Code and the tools are available.
|
|
67
|
+
|
|
68
|
+
To set a workspace directory (where documents are saved):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
claude mcp add texflow -- uvx texflow-mcp ~/Documents/TeXFlow
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Configure with Claude Desktop
|
|
75
|
+
|
|
76
|
+
Add to your `claude_desktop_config.json`:
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"mcpServers": {
|
|
81
|
+
"texflow": {
|
|
82
|
+
"command": "uvx",
|
|
83
|
+
"args": ["texflow-mcp"]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## How it works
|
|
90
|
+
|
|
91
|
+
TeXFlow has two entry flows, both converging on the same in-memory document model:
|
|
92
|
+
|
|
93
|
+
1. **Scaffold** — `document(action="create")` builds an empty document skeleton. Add content with `edit(action="insert")`.
|
|
94
|
+
2. **Markdown ingest** — `document(action="ingest", source="paper.md")` parses markdown into the model. Refine layout from there.
|
|
95
|
+
|
|
96
|
+
The model auto-saves to disk as JSON. LaTeX is only ever an output artifact — you never edit `.tex` directly.
|
|
97
|
+
|
|
98
|
+
## Tools
|
|
99
|
+
|
|
100
|
+
| Tool | Purpose |
|
|
101
|
+
|------|---------|
|
|
102
|
+
| `document` | Create, ingest markdown, show outline, read sections |
|
|
103
|
+
| `layout` | Columns, fonts, paper, margins, headers/footers, TOC |
|
|
104
|
+
| `edit` | Insert, replace, delete, move blocks (section, paragraph, figure, table, code, equation, list, raw) |
|
|
105
|
+
| `render` | Compile to PDF, preview page as PNG, export `.tex` |
|
|
106
|
+
| `reference` | Search LaTeX commands, symbols, packages, error help |
|
|
107
|
+
| `queue` | Batch multiple operations in one call |
|
|
108
|
+
|
|
109
|
+
Every response includes a workflow state hint showing where you are and what to do next.
|
|
110
|
+
|
|
111
|
+
## Example session
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
> document(action="create", title="My Paper", document_class="article")
|
|
115
|
+
|
|
116
|
+
> queue(operations=[
|
|
117
|
+
{"tool": "edit", "action": "insert", "block_type": "section", "title": "Introduction", "level": 1},
|
|
118
|
+
{"tool": "edit", "action": "insert", "content": "This paper explores...", "section": "Introduction"},
|
|
119
|
+
{"tool": "edit", "action": "insert", "block_type": "section", "title": "Methods", "level": 1},
|
|
120
|
+
{"tool": "layout", "font": "palatino", "columns": 2}
|
|
121
|
+
])
|
|
122
|
+
|
|
123
|
+
> render(action="compile")
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
git clone https://github.com/aaronsb/texflow-mcp
|
|
130
|
+
cd texflow-mcp
|
|
131
|
+
uv sync
|
|
132
|
+
uv run pytest tests/ -v # 222 tests
|
|
133
|
+
uv run texflow # Start MCP server
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# TeXFlow MCP
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A LaTeX document compiler with an [MCP](https://modelcontextprotocol.io) interface. AI agents operate on a structured document model — sections, paragraphs, figures, tables — while TeXFlow handles all LaTeX mechanics: packages, preamble, fonts, and compilation.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install texflow-mcp
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Or run without installing:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uvx texflow-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### System dependencies (optional)
|
|
22
|
+
|
|
23
|
+
TeXFlow compiles documents to PDF using XeLaTeX. Without it, you can still build and export `.tex` files.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# Arch
|
|
27
|
+
pacman -S texlive-xetex texlive-fontsrecommended texlive-fontsextra
|
|
28
|
+
|
|
29
|
+
# Debian/Ubuntu
|
|
30
|
+
apt install texlive-xetex texlive-fonts-recommended texlive-fonts-extra
|
|
31
|
+
|
|
32
|
+
# Fedora
|
|
33
|
+
dnf install texlive-xetex texlive-collection-fontsrecommended texlive-collection-fontsextra
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
For page preview (PNG), install `poppler-utils` (provides `pdftoppm`).
|
|
37
|
+
|
|
38
|
+
## Configure with Claude Code
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
claude mcp add texflow -- uvx texflow-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
That's it. Restart Claude Code and the tools are available.
|
|
45
|
+
|
|
46
|
+
To set a workspace directory (where documents are saved):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
claude mcp add texflow -- uvx texflow-mcp ~/Documents/TeXFlow
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Configure with Claude Desktop
|
|
53
|
+
|
|
54
|
+
Add to your `claude_desktop_config.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"texflow": {
|
|
60
|
+
"command": "uvx",
|
|
61
|
+
"args": ["texflow-mcp"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## How it works
|
|
68
|
+
|
|
69
|
+
TeXFlow has two entry flows, both converging on the same in-memory document model:
|
|
70
|
+
|
|
71
|
+
1. **Scaffold** — `document(action="create")` builds an empty document skeleton. Add content with `edit(action="insert")`.
|
|
72
|
+
2. **Markdown ingest** — `document(action="ingest", source="paper.md")` parses markdown into the model. Refine layout from there.
|
|
73
|
+
|
|
74
|
+
The model auto-saves to disk as JSON. LaTeX is only ever an output artifact — you never edit `.tex` directly.
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
| Tool | Purpose |
|
|
79
|
+
|------|---------|
|
|
80
|
+
| `document` | Create, ingest markdown, show outline, read sections |
|
|
81
|
+
| `layout` | Columns, fonts, paper, margins, headers/footers, TOC |
|
|
82
|
+
| `edit` | Insert, replace, delete, move blocks (section, paragraph, figure, table, code, equation, list, raw) |
|
|
83
|
+
| `render` | Compile to PDF, preview page as PNG, export `.tex` |
|
|
84
|
+
| `reference` | Search LaTeX commands, symbols, packages, error help |
|
|
85
|
+
| `queue` | Batch multiple operations in one call |
|
|
86
|
+
|
|
87
|
+
Every response includes a workflow state hint showing where you are and what to do next.
|
|
88
|
+
|
|
89
|
+
## Example session
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
> document(action="create", title="My Paper", document_class="article")
|
|
93
|
+
|
|
94
|
+
> queue(operations=[
|
|
95
|
+
{"tool": "edit", "action": "insert", "block_type": "section", "title": "Introduction", "level": 1},
|
|
96
|
+
{"tool": "edit", "action": "insert", "content": "This paper explores...", "section": "Introduction"},
|
|
97
|
+
{"tool": "edit", "action": "insert", "block_type": "section", "title": "Methods", "level": 1},
|
|
98
|
+
{"tool": "layout", "font": "palatino", "columns": 2}
|
|
99
|
+
])
|
|
100
|
+
|
|
101
|
+
> render(action="compile")
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Development
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
git clone https://github.com/aaronsb/texflow-mcp
|
|
108
|
+
cd texflow-mcp
|
|
109
|
+
uv sync
|
|
110
|
+
uv run pytest tests/ -v # 222 tests
|
|
111
|
+
uv run texflow # Start MCP server
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "texflow-mcp"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "LaTeX document compiler with MCP interface"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
authors = [
|
|
9
|
+
{name = "Aaron Bockelie", email = "aaronsb@users.noreply.github.com"},
|
|
10
|
+
]
|
|
11
|
+
keywords = ["mcp", "latex", "document", "typesetting", "pdf"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 4 - Beta",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Topic :: Text Processing :: Markup :: LaTeX",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"fastmcp>=3.0.0",
|
|
21
|
+
"mistune>=3.2.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/aaronsb/texflow-mcp"
|
|
26
|
+
Repository = "https://github.com/aaronsb/texflow-mcp"
|
|
27
|
+
Issues = "https://github.com/aaronsb/texflow-mcp/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
texflow = "server:main"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["setuptools", "wheel"]
|
|
34
|
+
build-backend = "setuptools.build_meta"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools]
|
|
37
|
+
packages = {find = {where = ["."], include = ["texflow*"]}}
|
|
38
|
+
py-modules = ["server"]
|
|
39
|
+
include-package-data = true
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.package-data]
|
|
42
|
+
texflow = ["data/**/*"]
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"pytest>=9.0.2",
|
|
50
|
+
]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""TeXFlow MCP server — LaTeX document compiler with MCP interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
|
|
10
|
+
from texflow.tools.state import set_output_dir
|
|
11
|
+
from texflow.tools.document import document_tool
|
|
12
|
+
from texflow.tools.layout import layout_tool
|
|
13
|
+
from texflow.tools.edit import edit_tool
|
|
14
|
+
from texflow.tools.render import render_tool
|
|
15
|
+
from texflow.tools.reference import reference_tool
|
|
16
|
+
from texflow.tools.queue import queue_tool
|
|
17
|
+
from texflow.tools.workflow import state_hint, error_hint, WORKFLOW_MAP
|
|
18
|
+
|
|
19
|
+
mcp = FastMCP(
|
|
20
|
+
"texflow",
|
|
21
|
+
instructions=(
|
|
22
|
+
"TeXFlow is a LaTeX document compiler. "
|
|
23
|
+
"Use 'document' to create or ingest content, 'layout' to configure typesetting, "
|
|
24
|
+
"'edit' to manipulate content structurally, 'render' to compile to PDF, "
|
|
25
|
+
"'reference' for LaTeX documentation search, and "
|
|
26
|
+
"'queue' to batch multiple operations in one call. "
|
|
27
|
+
"Every response includes a workflow state hint showing where you are and what to do next."
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _with_hints(result: str) -> str:
|
|
33
|
+
"""Append workflow state hint and error guidance to a tool result."""
|
|
34
|
+
extra = error_hint(result)
|
|
35
|
+
return result + extra + state_hint()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# --- Workflow resource ---
|
|
39
|
+
|
|
40
|
+
@mcp.resource("texflow://workflow")
|
|
41
|
+
def workflow_map() -> str:
|
|
42
|
+
"""TeXFlow workflow map showing states and transitions."""
|
|
43
|
+
return WORKFLOW_MAP + "\n" + state_hint()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# --- Document tool ---
|
|
47
|
+
|
|
48
|
+
@mcp.tool()
|
|
49
|
+
def document(
|
|
50
|
+
action: str,
|
|
51
|
+
document_class: str | None = None,
|
|
52
|
+
title: str | None = None,
|
|
53
|
+
author: str | None = None,
|
|
54
|
+
source: str | None = None,
|
|
55
|
+
section: str | None = None,
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Create, ingest, and inspect documents.
|
|
58
|
+
|
|
59
|
+
Actions:
|
|
60
|
+
- create: Scaffold a new empty document. Optionally set class, title, author.
|
|
61
|
+
- ingest: Parse markdown text or file path into the document model.
|
|
62
|
+
- outline: Show document structure (sections, block counts).
|
|
63
|
+
- read: Read content of a specific section as prose text.
|
|
64
|
+
"""
|
|
65
|
+
return _with_hints(document_tool(action, document_class, title, author, source, section))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --- Layout tool ---
|
|
69
|
+
|
|
70
|
+
@mcp.tool()
|
|
71
|
+
def layout(
|
|
72
|
+
columns: int | None = None,
|
|
73
|
+
font: str | None = None,
|
|
74
|
+
font_sans: str | None = None,
|
|
75
|
+
font_mono: str | None = None,
|
|
76
|
+
font_size: str | None = None,
|
|
77
|
+
paper: str | None = None,
|
|
78
|
+
margins: str | None = None,
|
|
79
|
+
header_left: str | None = None,
|
|
80
|
+
header_center: str | None = None,
|
|
81
|
+
header_right: str | None = None,
|
|
82
|
+
footer_left: str | None = None,
|
|
83
|
+
footer_center: str | None = None,
|
|
84
|
+
footer_right: str | None = None,
|
|
85
|
+
toc: bool | None = None,
|
|
86
|
+
lof: bool | None = None,
|
|
87
|
+
lot: bool | None = None,
|
|
88
|
+
line_spacing: float | None = None,
|
|
89
|
+
) -> str:
|
|
90
|
+
"""Configure document typesetting and layout.
|
|
91
|
+
|
|
92
|
+
Only provided parameters are changed; others are left as-is.
|
|
93
|
+
Returns the current full layout configuration after changes.
|
|
94
|
+
"""
|
|
95
|
+
return _with_hints(layout_tool(
|
|
96
|
+
columns, font, font_sans, font_mono, font_size, paper, margins,
|
|
97
|
+
header_left, header_center, header_right,
|
|
98
|
+
footer_left, footer_center, footer_right,
|
|
99
|
+
toc, lof, lot, line_spacing,
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# --- Edit tool ---
|
|
104
|
+
|
|
105
|
+
@mcp.tool()
|
|
106
|
+
def edit(
|
|
107
|
+
action: str,
|
|
108
|
+
block_type: str | None = None,
|
|
109
|
+
section: str | None = None,
|
|
110
|
+
position: int | None = None,
|
|
111
|
+
content: str | None = None,
|
|
112
|
+
title: str | None = None,
|
|
113
|
+
level: int | None = None,
|
|
114
|
+
language: str | None = None,
|
|
115
|
+
path: str | None = None,
|
|
116
|
+
caption: str | None = None,
|
|
117
|
+
headers: list[str] | None = None,
|
|
118
|
+
rows: list[list[str]] | None = None,
|
|
119
|
+
target_section: str | None = None,
|
|
120
|
+
target_position: int | None = None,
|
|
121
|
+
) -> str:
|
|
122
|
+
"""Manipulate document content structurally.
|
|
123
|
+
|
|
124
|
+
Actions:
|
|
125
|
+
- insert: Add a new block at a position within a section.
|
|
126
|
+
- replace: Replace a block at a position with new content.
|
|
127
|
+
- delete: Remove a block at a position.
|
|
128
|
+
- move: Move a block from one location to another.
|
|
129
|
+
|
|
130
|
+
Sections are addressed by title path (e.g., 'Methods/Data Collection').
|
|
131
|
+
Blocks within a section are addressed by 0-based index.
|
|
132
|
+
Use document(action='outline') to see current structure and indices.
|
|
133
|
+
"""
|
|
134
|
+
return _with_hints(edit_tool(
|
|
135
|
+
action, block_type, section, position, content, title, level,
|
|
136
|
+
language, path, caption, headers, rows, target_section, target_position,
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# --- Render tool ---
|
|
141
|
+
|
|
142
|
+
@mcp.tool()
|
|
143
|
+
def render(
|
|
144
|
+
action: str,
|
|
145
|
+
output_path: str | None = None,
|
|
146
|
+
page: int | None = None,
|
|
147
|
+
dpi: int | None = None,
|
|
148
|
+
) -> str:
|
|
149
|
+
"""Compile and export the document.
|
|
150
|
+
|
|
151
|
+
Actions:
|
|
152
|
+
- compile: Serialize model to .tex, compile to PDF. Returns PDF path.
|
|
153
|
+
- preview: Render a specific page as base64 PNG image.
|
|
154
|
+
- tex: Export the raw .tex source. Returns the LaTeX content.
|
|
155
|
+
"""
|
|
156
|
+
return _with_hints(render_tool(action, output_path, page, dpi))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# --- Reference tool ---
|
|
160
|
+
|
|
161
|
+
@mcp.tool()
|
|
162
|
+
def reference(
|
|
163
|
+
action: str,
|
|
164
|
+
query: str | None = None,
|
|
165
|
+
description: str | None = None,
|
|
166
|
+
name: str | None = None,
|
|
167
|
+
error: str | None = None,
|
|
168
|
+
topic: str | None = None,
|
|
169
|
+
path: str | None = None,
|
|
170
|
+
) -> str:
|
|
171
|
+
"""Search LaTeX documentation, symbols, packages, and error solutions.
|
|
172
|
+
|
|
173
|
+
Actions:
|
|
174
|
+
- search: Search for LaTeX commands or general topics.
|
|
175
|
+
- symbol: Find symbols by description (e.g., "approximately equal").
|
|
176
|
+
- package: Get information about a LaTeX package.
|
|
177
|
+
- check_style: Analyze a .tex file for best practices.
|
|
178
|
+
- error_help: Get help for LaTeX error messages.
|
|
179
|
+
- example: Get working examples for a topic (table, equation, figure, list, code).
|
|
180
|
+
"""
|
|
181
|
+
return _with_hints(reference_tool(action, query, description, name, error, topic, path))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# --- Queue tool ---
|
|
185
|
+
|
|
186
|
+
@mcp.tool()
|
|
187
|
+
def queue(
|
|
188
|
+
operations: list[dict],
|
|
189
|
+
continue_on_error: bool = False,
|
|
190
|
+
) -> str:
|
|
191
|
+
"""Execute multiple operations in a single call.
|
|
192
|
+
|
|
193
|
+
Each operation is a dict with 'tool' (document, layout, edit, render, reference)
|
|
194
|
+
plus the arguments for that tool. Operations run sequentially; disk is written
|
|
195
|
+
once at the end.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
queue(operations=[
|
|
199
|
+
{"tool": "document", "action": "create", "title": "My Doc"},
|
|
200
|
+
{"tool": "edit", "action": "insert", "block_type": "section", "title": "Intro", "level": 1},
|
|
201
|
+
{"tool": "edit", "action": "insert", "content": "Hello world.", "section": "Intro"},
|
|
202
|
+
{"tool": "layout", "columns": 2, "font": "palatino"}
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
operations: List of operation dicts.
|
|
207
|
+
continue_on_error: If False (default), stop on first error.
|
|
208
|
+
"""
|
|
209
|
+
# Queue gets summary + state hint, no per-operation hints
|
|
210
|
+
result = queue_tool(operations, continue_on_error)
|
|
211
|
+
return result + state_hint()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main():
|
|
215
|
+
"""Entry point for the texflow CLI."""
|
|
216
|
+
# Optional workspace dir argument
|
|
217
|
+
if len(sys.argv) > 1:
|
|
218
|
+
workspace = Path(sys.argv[1])
|
|
219
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
set_output_dir(workspace)
|
|
221
|
+
|
|
222
|
+
mcp.run()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Tests for the LaTeX compiler."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from texflow.compiler import CompileError, CompileResult, compile_tex, preview_page, _parse_errors
|
|
10
|
+
from texflow.model import Document, Metadata, Paragraph, Section
|
|
11
|
+
from texflow.serializer import serialize
|
|
12
|
+
|
|
13
|
+
HAS_XELATEX = shutil.which("xelatex") is not None
|
|
14
|
+
HAS_PDFLATEX = shutil.which("pdflatex") is not None
|
|
15
|
+
HAS_LATEX = HAS_XELATEX or HAS_PDFLATEX
|
|
16
|
+
HAS_PDFTOPPM = shutil.which("pdftoppm") is not None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _minimal_tex() -> str:
|
|
20
|
+
doc = Document(
|
|
21
|
+
metadata=Metadata(title="Test"),
|
|
22
|
+
content=[Section(title="Hello", level=1, content=[Paragraph(text="World")])],
|
|
23
|
+
)
|
|
24
|
+
return serialize(doc)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_compile_result_dataclass():
|
|
28
|
+
r = CompileResult(success=True, pdf_path=Path("/tmp/test.pdf"))
|
|
29
|
+
assert r.success is True
|
|
30
|
+
assert r.errors == []
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_parse_errors_from_log():
|
|
34
|
+
log = """This is some log output
|
|
35
|
+
! Undefined control sequence.
|
|
36
|
+
l.42 \\badcommand
|
|
37
|
+
|
|
38
|
+
! Missing $ inserted.
|
|
39
|
+
l.55 some_text_
|
|
40
|
+
"""
|
|
41
|
+
errors = _parse_errors(log)
|
|
42
|
+
assert len(errors) == 2
|
|
43
|
+
assert errors[0].message == "Undefined control sequence."
|
|
44
|
+
assert errors[0].line == 42
|
|
45
|
+
assert errors[1].message == "Missing $ inserted."
|
|
46
|
+
assert errors[1].line == 55
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_parse_errors_empty_log():
|
|
50
|
+
assert _parse_errors("All good, no errors here.") == []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.skipif(not HAS_LATEX, reason="No LaTeX engine installed")
|
|
54
|
+
def test_compile_minimal_document():
|
|
55
|
+
tex = _minimal_tex()
|
|
56
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
57
|
+
result = compile_tex(tex, output_dir=Path(tmpdir), filename="test")
|
|
58
|
+
assert result.tex_path is not None
|
|
59
|
+
assert result.tex_path.exists()
|
|
60
|
+
if result.success:
|
|
61
|
+
assert result.pdf_path is not None
|
|
62
|
+
assert result.pdf_path.exists()
|
|
63
|
+
assert result.pdf_path.stat().st_size > 0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.skipif(not HAS_LATEX, reason="No LaTeX engine installed")
|
|
67
|
+
def test_compile_with_errors():
|
|
68
|
+
bad_tex = "\\documentclass{article}\n\\begin{document}\n\\badcommand\n\\end{document}"
|
|
69
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
70
|
+
result = compile_tex(bad_tex, output_dir=Path(tmpdir), filename="bad")
|
|
71
|
+
assert result.tex_path is not None
|
|
72
|
+
assert len(result.errors) > 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.mark.skipif(not (HAS_LATEX and HAS_PDFTOPPM), reason="Requires LaTeX + pdftoppm")
|
|
76
|
+
def test_preview_page():
|
|
77
|
+
tex = _minimal_tex()
|
|
78
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
79
|
+
result = compile_tex(tex, output_dir=Path(tmpdir))
|
|
80
|
+
if result.success and result.pdf_path:
|
|
81
|
+
b64 = preview_page(result.pdf_path, page=1, dpi=72)
|
|
82
|
+
assert b64 is not None
|
|
83
|
+
assert len(b64) > 100 # Should be a non-trivial PNG
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_preview_nonexistent_file():
|
|
87
|
+
result = preview_page(Path("/tmp/nonexistent.pdf"))
|
|
88
|
+
assert result is None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@pytest.mark.skipif(HAS_LATEX, reason="Only test when no LaTeX engine available")
|
|
92
|
+
def test_compile_without_engine():
|
|
93
|
+
tex = _minimal_tex()
|
|
94
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
95
|
+
result = compile_tex(tex, output_dir=Path(tmpdir))
|
|
96
|
+
assert result.success is False
|
|
97
|
+
assert result.tex_path is not None
|
|
98
|
+
assert result.tex_path.exists()
|
|
99
|
+
assert any("No LaTeX engine" in e.message for e in result.errors)
|