macaulay2-mcp
# macaulay2-mcp
## Motivation
The goal for the Macaulay2-MCP project is to leverage large-language-models when working with [Macaulay2](https://macaulay2.com). Especially, it should lower the entry point for newcomers or test a few lines of code. It is still under development; use it at your own risk and share your thoughts with me.
The current version is **under testing**. This early version is published on GitHub to test other components such as PyPI, LM Studio linking, and Binder.
## Philosophy
- Nearly all commands and programs below are non-invasive and reversible.
- The program is not designed to make decisions but inform the users of options.
- At some point, this MCP should work together with Lean-LSP.
- It will be updated on an as-needed basis.
## Acknowledgments
The project is largely developed under the author's guidance by Qwen 3.8 models hosted at [National Research Platform](https://nrp.ai). Also, the author thanks Dr. Mary Thomas and other leaders at the San Diego Supercomputer Center. The author learned a large portion of the skills used in this project from their CyberInfrastructure Professional Development Training Program.
## Introduction
Use [Macaulay2](https://macaulay2.com) from your AI coding assistant.
`macaulay2-mcp` is an [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server that gives Claude Code, [opencode](https://opencode.ai), [LM Studio](https://lmstudio.ai), and any other MCP client a **persistent Macaulay2 session**: ask your assistant to compute Groebner bases, resolutions, Betti tables, primary decompositions, Hilbert polynomials — and it runs the computations in a real M2 kernel, with state (rings, ideals, your definitions) preserved across calls.
Three notations, one kernel — raw M2 syntax, plain English, LaTeX. Each ask is self-contained (paste any one into a fresh session), and the trailing memory line is genuine output (every `m2_evaluate` result ends with the kernel's resident/peak):
[](https://github.com/user-attachments/assets/570d928c-f2bd-4f6e-aa6c-eb07439b83c3)
Note: When running the examples below, using Macaulay2 skill at <https://github.com/youngsu-Kim/macaulay2-skills> (`skills/macaulay2/SKILL.md`) would be helpful. That is, start running example with "First read Macaulay2-skills available at https://github.com/youngsu-Kim/macaulay2-skills. Then compute the Groebner basis of ideal(x^3 - y, x^4 - z) in QQ[x,y,z]"
Input
```text
Compute the Groebner basis of ideal(x^3 - y, x^4 - z) in QQ[x,y,z]
```
Or Input
```text
First read Macaulay2-skills available at https://github.com/youngsu-Kim/macaulay2-skills. Then compute the Groebner basis of ideal(x^3 - y, x^4 - z) in QQ[x,y,z]
```
Output Example
```text
Response: m2_evaluate: R = QQ[x,y,z]; I = ideal(x^3 - y, x^4 - z)
m2_evaluate: print generators (gb I)
The reduced Groebner basis is
{xy - z, x^2 z - y^2, y^3 - x z^2, x^3 - y}.
(M2 memory: 118.5 MB resident, peak 103.0 MB)
```
---
Input
```text
In the polynomial ring in x, y, z over the rationals, let J be the ideal generated by x squared plus y squared minus z squared.
Use macaulay2-mcp to verify: is J prime? What are the dimension and degree of the quotient?
```
Output Example
```text
Response: m2_evaluate: S = QQ[x,y,z]; J = ideal(x^2 + y^2 - z^2)
m2_evaluate: isPrime J; dim (S/J); degree (S/J)
J is prime; S/J is a surface (dimension 2) of degree 2.
(M2 memory: 119.3 MB resident, peak 103.0 MB)
```
---
Input
```text
For the twisted cubic $C \subset \mathbb{P}^3$, whose ideal is $I_C = (xz - y^2, yw - z^2, xw - yz)$, compute a free resolution of $S/I_C$ with Betti table, and the Hilbert polynomial of $C$, in Macaulay2
```
Output Example
```text
Response: m2_evaluate: A = QQ[w,x,y,z]
C = ideal(x*z - y^2, y*w - z^2, x*w - y*z)
m2_evaluate: betti res C
m2_evaluate: hilbertPolynomial (A^1/C)
Totals (1, 3, 2) — three quadrics, two linear syzygies — and the
Hilbert polynomial 3T + 1.
(M2 memory: 118.5 MB resident, peak 103.0 MB)
```
M2 syntax helps weak AI models, LaTeX/plain words usually suffice for strong ones — your assistant does the translating; verify its input lines.
Two design notes for local models: on multi-part asks, heavy-thinking models can spend minutes and many self-correcting tool calls — correct but slow (real transcripts: Case study 2 in [`examples/latex-decomposition-test.md`](examples/latex-decomposition-test.md)); and a famous object invites *answering from memory without calling the tool at all* — hence ask 1 speaks M2's own syntax while asks 2 and 3 state it outright ("Use macaulay2-mcp to verify…", "…in Macaulay2"). And note that graded invariants (Betti tables, Hilbert polynomials) are asked only of a *homogeneous* ideal — where M2's table is minimal and unarguable.
---
## Installation
**Prerequisites (one line each):**
| You need | macOS | Ubuntu |
|---|---|---|
| Macaulay2 (latest stable, 1.26) | `brew install Macaulay2/tap/macaulay2` | `sudo add-apt-repository ppa:macaulay2/macaulay2 && sudo apt install macaulay2` |
| [uv](https://docs.astral.sh/uv/) (runs the server, no install) | `brew install uv` | `curl -LsSf https://astral.sh/uv/install.sh \| sh` |
*Why a tap/PPA, how to undo it, the alternative uv installer, and the full list of environment settings: [docs/installation.md](docs/installation.md).*
**Add it to your AI client (harness):**
* **Claude Code**
```sh
claude mcp add macaulay2 -- uvx macaulay2-mcp
```
* **opencode** — add to your `opencode.json` (or project `opencode.json`):
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"macaulay2": {
"type": "local",
"command": ["uvx", "macaulay2-mcp"]
}
}
}
```
* **LM Studio (GUI chat)** — LM Studio (≥ 0.4.24) is itself an MCP host: Program tab → **Install → Edit mcp.json** → paste, then enable the server and pick a **tool-calling-capable** model (start it with the in-chat 1+1 check under **Check the MCP** below).
```json
{
"mcpServers": {
"macaulay2": {
"command": "uvx",
"args": ["macaulay2-mcp"],
"env": { "MACAULAY2_MCP_JOURNAL": "~/.local/share/macaulay2-mcp/journals" }
}
}
}
```
The `env` line just pins the history file to a known folder — see [Reading the journal](docs/journal.md) to relocate or turn it off. Setup details and fixes: [Troubleshooting](docs/troubleshooting.md).
**Check the MCP:**
```sh
uvx macaulay2-mcp selftest
```
```text
macaulay2-mcp 0.1.4 self-test
[OK] found Macaulay2: /opt/homebrew/bin/M2
[OK] supported version (1.26.x): 1.26.06
[OK] started session: prompt received
[OK] evaluated 1 + 1: 1 + 1 / o1 = 2
Self-test passed. The MCP server is ready to use.
```
If anything above fails: M2 not on PATH → set `M2_BIN=/path/to/M2` in the client's environment; both prereq commands add third-party repos (reversible — see [docs/installation.md](docs/installation.md)); more symptoms and fixes: [Troubleshooting](docs/troubleshooting.md).
**From inside the chat** — checks that *your* model can reach the server (and wakes a lazily-started one). Paste as your first message:
```text
Before any real math: use the Macaulay2 tool to evaluate 1 + 1, then report
the tool's last line verbatim. If you don't see a Macaulay2 tool, say so
plainly — don't invent an answer.
```
## Examples
With the server connected, just ask (in Claude Code / opencode / ...). All of these are tested prompts:
For a model that struggles with M2's dialect, prepend one line to any prompt below: *"Read the Macaulay2 skill at <https://github.com/youngsu-Kim/macaulay2-skills> (`skills/macaulay2/SKILL.md`) and follow it before executing."* Agents that installed the skill via `npx skills add youngsu-Kim/macaulay2-skills` do this without being told — see its FINDINGS.md for the measured effect across five model families.
* “Create `R = QQ[x,y,z]` and `I = ideal(x^3 - y, x^4 - z)`. Compute the Groebner basis and a free resolution; show the Betti table, and the dimensions of `R` and `R/I`.” *(→ basis `xy - z, x^2 z - y^2, y^3 - x z^2, x^3 - y`; M2 prints totals `1 4 4 1`; dims `3` and `1` — the monomial curve is a curve)*
* “Use M2's own library: load the `BoijSoederberg` package and decompose the Betti diagram of `res I` into pure diagrams (`decomposeBetti`); also look up `hilbertPolynomial` and compute it for the twisted cubic `(x*z - y^2, y*w - z^2, x*w - y*z)`.” *(→ a positive sum of 3 pure diagrams; `3T + 1`, printed `- 2*P₀ + 3*P₁`. Note the lowercase `h`: M2's CamelCase doc pointer `HilbertPolynomial` is an empty stub.)*
* “Compute the primary decomposition of `ideal(x^2, x*y)`. Then work through Macaulay2's official *Getting Started* example — the rational quartic `monomialCurveIdeal(R,{1,3,4})`: dimension, degree, Hilbert polynomial, resolution, Betti table.” *(→ `(x) ∩ (x², y)`; quartic: degree 4, Hilbert `4T + 1`, totals (1, 4, 4, 1) — straight from the [tutorial itself](https://macaulay2.com/GettingStarted/); it works over MCP unchanged)*
* “For the family `I_k = (x^(k+2) - y, x^(k+3) - z)` in `QQ[x,y,z]`, loop over `k = 1..6` and tabulate the reduced Groebner basis sizes and the dimensions of `R/I_k` — then fan the same family out across several subagents as independent batch jobs and collect the results.” *(→ sizes k+3, dim 1 throughout; the batch jobs are real parallel M2 processes)*
* “Here is my `mycode.m2` file — import it into the session and call `myFunction`.” *(state is kept between calls)*
A full genuine transcript of the first prompt's Groebner/resolution ask: [`examples/groebner-demo.md`](examples/groebner-demo.md). How the server itself handles long runs, stops, parallel batches, and errors: [`examples/example-prompts.md`](examples/example-prompts.md) (§§2–5).
Want to benchmark your own model the way a real user types math? [`examples/latex-decomposition-test.md`](examples/latex-decomposition-test.md) gives you two ready prompts, a rubric with known-true answers, and what we measured.
The companion [macaulay2-skills](https://github.com/youngsu-Kim/macaulay2-skills) repo ships machine-verified M2 1.26 know-how as an Agent Skill (`npx skills add youngsu-Kim/macaulay2-skills`) — it pairs with this server but is not bound to it, and installs into Claude Code, OpenCode, or LM Studio. Measured here on the three Introduction examples: a thinking-disabled model went from ~22 kernel calls with 8 error/retry events to 5 clean calls, and across all four thinking levels the skill held 3–8 calls with zero errors; its **[Benchmark at a glance](https://github.com/youngsu-Kim/macaulay2-skills#benchmark-at-a-glance)** table carries the dated, fully-versioned cross-deployment result (full protocol and transcripts in [FINDINGS.md](https://github.com/youngsu-Kim/macaulay2-skills/blob/main/FINDINGS.md)). The result generalized to five more model configurations (qwen3-small, DeepSeek-V4-Flash, GLM-5, Kimi, GPT-OSS): 3–5 clean calls each, 32 of 33 graded subanswers correct — the single miss (reading the Hilbert class form `-2*P₀ + 3*P₁`, by GPT-OSS) became the skill's trap #17 — and globally installed sessions discover the skill with no pointer at all.
### From your own code (no LLM required)
The server is an ordinary MCP stdio process, so plain Python can drive the same persistent kernel: [`examples/drive_with_python.py`](examples/drive_with_python.py) (`uv run python examples/drive_with_python.py`) shows state surviving between separate tool calls, times each call client-side, and reports kernel memory two ways — the per-call resident/peak line and the on-demand `m2_memory()` tool, with a computation that visibly moves the peak.
## What the server provides
Nine tools, one shared M2 session:
| Tool | What it does |
|---|---|
| `m2_evaluate(code, timeout_s?, stop_on_error?, show_memory?)` | Evaluate M2 code in the persistent session. State carries over between calls; `stop_on_error=True` halts at the first error instead of running the rest. Results end with a kernel-memory line (resident + peak; swap when paging) unless `show_memory=False`. |
| `m2_interrupt()` | Stop a running computation: M2 aborts at a safe checkpoint and **keeps** all earlier definitions (unlike a timeout, which restarts the kernel). |
| `m2_memory()` | Report the kernel's resident memory (RSS), peak, swap, pid, and uptime — an unprivileged OS read of the server's own child process; works **while** a computation runs, so you can watch before deciding to interrupt. |
| `m2_session_reset()` | Restart the kernel — a clean slate. |
| `m2_help(topic)` | M2 documentation lookup (`help "topic"`). |
| `m2_run_script(path, timeout_s?)` | Run a `.m2` file in a fresh, isolated M2 process (batch mode; use `print` for output). |
| `m2_list_packages()` | List packages currently loaded in the session. |
| `m2_load_package(name, reload?)` | Load a package (e.g. `HilbertSchemes`, `CommutativeAlgebra`). |
| `m2_import_file(path)` | Import a local `.m2` file into the session — newly defined or updated commands are picked up without a restart. |
## How it works (and the safety limits)
The server keeps one Macaulay2 kernel alive and sends your code to it, exactly like Emacs does. Results, M2 errors, and warnings all come back in the tool output, so your assistant can read and react to them.
* **Version pin.** v0.1 supports **Macaulay2 1.26.x (the current latest stable at release time) only**. Other versions produce a clear error with the upgrade command.
* **Timeout guard.** `m2_evaluate` and `m2_run_script` are guarded by an author-set default of **120 seconds** (raise per call up to 3600) against runaway or infinite computations. A timeout is *not* an M2 error: the message says so, and explains how to retry with a larger `timeout_s` (self-contained code, since the session is restarted).
* **Stopping on demand.** `m2_interrupt` sends a real software interrupt (SIGINT): M2 aborts the current computation at a safe checkpoint and the running call returns with `error: interrupted` — **all earlier definitions survive**. Only the timeout backstop (for computations that ignore the interrupt) restarts the kernel and loses state.
* **Parallelism.** The shared session serializes evaluations by design (one kernel = consistent state; safe for concurrent requests from subagents). Genuine concurrency today: every `m2_run_script` spawns its own M2 process and multiple jobs run in parallel — e.g. one subagent per slice of an ideal family. First-class job submission (`m2_submit_job`, status/wait/cancel over a kernel pool) is planned.
* **Errors inform, they don't decide.** M2 is a REPL: a runtime error does *not* stop the remaining lines from running, and there is no rollback. When that happens, the tool result appends an explicit menu — CONTINUE (fix and resend just the failing statement), RESTART (session reset — irreversible, all definitions lost), or INSPECT (see what survived) — and your assistant is instructed to put those choices to *you*. To prevent the cascade up front, run blocks with `stop_on_error=True`.
* **Unbalanced input** (e.g. a missing `}`) is rejected up front instead of hanging, and syntax errors that desynchronize the session trigger an automatic restart.
* **OS-access gate.** M2 functions that run programs, touch the filesystem, reach the network, or kill the kernel (`runProgram`, `lines`, `openOut`, `makeDirectory`, `installPackage`, `quit`, …) are **refused before anything executes** — the session stays untouched and the message explains how the user can enable a specific symbol (`MACAULAY2_MCP_OS_ALLOW=lines,openOut` in the server's environment). The gate is friction against accidents, not a sandbox: M2's `value("...")` string-evaluation is not blocked (blocking it breaks legitimate metaprogramming). For real isolation, run the server in a container/VM.
* **Audit journal.** Every MCP↔M2 exchange is appended to a JSONL file at `./.m2-mcp/session-<UTC>-<pid>.jsonl` in your project: the code, M2's output, timings, refused gate attempts, and the MCP client (LLM host) that connected. Relocate with `MACAULAY2_MCP_JOURNAL=<dir>`, disable with `=off`; for GUI clients a single central location is recommended, e.g. `~/.local/share/macaulay2-mcp/journals`. Add `.m2-mcp/` to your `.gitignore` (the server never reads it back in v0.1; checkpoint/replay is planned). Full guide: [Reading the journal](docs/journal.md).
* **Long results are excerpted.** A tool result past ~120 lines / 32 KB comes back as its first 100 and last 10 lines with a notice naming where the full text was saved (the journal); nothing is silently lost, and chats stay readable. Prefer narrowing in M2 (`take`, `drop`, smaller examples) over dumping huge results.
* **Memory reporting.** Every `m2_evaluate` result ends with the kernel's resident memory and its peak — pass `show_memory=False` to omit that line — and `m2_memory()` answers on demand, *including while a long computation is running* (a cheap watchdog: watch, then decide whether `m2_interrupt` is worthwhile). When the kernel starts paging to swap, the line names the swap amount and a note lays out the options; nothing is ever killed automatically. The journal records `rss_bytes`, `peak_rss_bytes` and `swap_bytes` per evaluation regardless of the flag, and a kernel killed by a timeout is probed just before the restart, so its last measurement survives. Peak uses kernel-tracked counters (`VmHWM` on Linux; `footprint`'s `phys_footprint_peak` on macOS), not sampling; swap is per-process on Linux, and the system-wide growth since the kernel started on macOS (Apple exposes no unprivileged per-process swap counter).
* **Security.** This remains a local tool: your assistant can run arbitrary M2 computation on your machine. Both Claude Code and opencode ask for your approval per tool call by default — keep it that way.
### Tested environments
Behaviour (including the golden outputs) is pinned against Macaulay2 1.26 on:
| Environment | Machine | Memory relevant to the swap guidance |
|---|---|---|
| Development | Apple-silicon Mac, macOS 27 | — |
| CI | GitHub Actions ubuntu-latest (24.04) | 16 GB |
| Binder demo | Container / hosted VM | Low single-digit GB, host-dependent |
### The OS-access gate
Why some perfectly normal-looking code is **blocked**, and where this safeguard's honest limits are: the full rationale lives in [`docs/os-access-gate.md`](docs/os-access-gate.md). In one line: M2 functions that run programs, touch the filesystem, reach the network, or kill the kernel are refused *before anything executes* — and since the check matches words rather than call positions, a plain variable named `lines` is refused too (rename it, or allowlist the symbol via `MACAULAY2_MCP_OS_ALLOW`). It is friction against accidents, not a sandbox.
## Design principles
1. **Local-first.** The server runs on your machine against your Macaulay2 installation. No accounts, no telemetry, no network calls.
2. **Messages inform, never direct.** Every message states what it does and whether (and how) it is reversible; we explain decisions instead of telling you to click through them.
3. **Errors carry their own fix.** "Not found" ships with install commands; "wrong version" ships with the exact upgrade line; timeouts explain the retry recipe.
4. **Safety limits are explicit.** Timeouts are author-set, labeled as such, distinguishable from real errors, and adjustable per call.
## Try it in your browser (no install)
[](https://mybinder.org/v2/gh/youngsu-Kim/macaulay2-mcp-binder/main?urlpath=lab/tree/demo.ipynb)
The companion repo [`m2-mcp-binder`](https://github.com/youngsu-Kim/macaulay2-mcp-binder) launches a JupyterLab session (first launch ~2–4 min) where a plain Python notebook drives this very server over MCP — a zero-install way to see the tools in action.
## Troubleshooting
Symptoms and fixes: [`docs/troubleshooting.md`](docs/troubleshooting.md).
## Todos/Plans
* Remote/HTTP mode (Streamable HTTP + API key) so the server can be hosted and connected to services such as ChatGPT web; deployment recipes (Docker, Cloudflare Tunnel).
* Multi-user sessions, support for older/newer M2 versions, MCP prompts for common workflows (e.g. "analyze an ideal"), a package availability search, and more — after community feedback.
* Session-state persistence across restarts.
* Dedicated dataset to train an LLM.
## Feedback
Please file issues and PRs on [GitHub](https://github.com/youngsu-Kim/macaulay2-mcp). Once the software has stabilized, it is expected to be announced in the [Macaulay2 Zulip](https://macaulay2.zulipchat.com/) first.
## License
This program is free software under the [GNU General Public License v3 or later](LICENSE): use it freely, including commercially — but if you distribute it, or a program built on top of it, the same freedoms must travel with your copy.
TDQS
Scored across 9 tools
Each tool targets a clearly distinct operation: persistent evaluation vs. isolated script execution vs. file import, package management, help, memory, and interrupt. The few potential overlaps (e.g., m2_evaluate vs. m2_import_file) are well-separated by input source and session-state semantics.
All tools use the same m2_ prefix and snake_case, making the set highly readable. Minor deviations from a strict verb_noun pattern appear in m2_session_reset (noun_verb) and single-word tools like m2_help, m2_memory, and m2_interrupt.
The 9 tools are well-scoped for controlling a Macaulay2 session. Each tool covers a discrete need (execute, batch, import, packages, help, reset, memory, interrupt) without redundancy.
The surface covers the full session lifecycle: code evaluation, batch execution, file import, package listing/loading, documentation lookup, kernel reset, memory monitoring, and interruption. No obvious gaps exist for the stated domain, and noted M2 limitations (e.g., no unload) are appropriately handled.