macaulay2-mcp 0.1.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.
- macaulay2_mcp-0.1.0/.dockerignore +14 -0
- macaulay2_mcp-0.1.0/.github/workflows/ci.yml +57 -0
- macaulay2_mcp-0.1.0/.github/workflows/release.yml +44 -0
- macaulay2_mcp-0.1.0/.gitignore +38 -0
- macaulay2_mcp-0.1.0/.mcp.json +8 -0
- macaulay2_mcp-0.1.0/AGENTS.md +128 -0
- macaulay2_mcp-0.1.0/CITATION.cff +10 -0
- macaulay2_mcp-0.1.0/LICENSE +674 -0
- macaulay2_mcp-0.1.0/PKG-INFO +254 -0
- macaulay2_mcp-0.1.0/README.md +237 -0
- macaulay2_mcp-0.1.0/e2e/Dockerfile +33 -0
- macaulay2_mcp-0.1.0/e2e/README.md +105 -0
- macaulay2_mcp-0.1.0/e2e/assert.py +93 -0
- macaulay2_mcp-0.1.0/e2e/docker-compose.gpu.yml +11 -0
- macaulay2_mcp-0.1.0/e2e/docker-compose.yml +31 -0
- macaulay2_mcp-0.1.0/e2e/opencode.native.json +28 -0
- macaulay2_mcp-0.1.0/e2e/results-14b/run.log +143 -0
- macaulay2_mcp-0.1.0/e2e/run_e2e.sh +132 -0
- macaulay2_mcp-0.1.0/e2e/soak.sh +45 -0
- macaulay2_mcp-0.1.0/e2e/task-latex.txt +7 -0
- macaulay2_mcp-0.1.0/e2e/task.txt +9 -0
- macaulay2_mcp-0.1.0/examples/example-prompts.md +398 -0
- macaulay2_mcp-0.1.0/examples/groebner-demo.md +72 -0
- macaulay2_mcp-0.1.0/examples/official-tutorial-run.md +203 -0
- macaulay2_mcp-0.1.0/pyproject.toml +44 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/__init__.py +3 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/__main__.py +4 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/cli.py +91 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/config.py +155 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/gatekeep.py +150 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/journal.py +146 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/kernel.py +621 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/scanner.py +61 -0
- macaulay2_mcp-0.1.0/src/macaulay2_mcp/server.py +447 -0
- macaulay2_mcp-0.1.0/tests/conftest.py +10 -0
- macaulay2_mcp-0.1.0/tests/data/golden.jsonl +17 -0
- macaulay2_mcp-0.1.0/tests/test_gatekeep.py +128 -0
- macaulay2_mcp-0.1.0/tests/test_golden.py +52 -0
- macaulay2_mcp-0.1.0/tests/test_journal.py +139 -0
- macaulay2_mcp-0.1.0/tests/test_kernel.py +311 -0
- macaulay2_mcp-0.1.0/tests/test_mcp_client.py +223 -0
- macaulay2_mcp-0.1.0/uv.lock +1083 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: tests (${{ matrix.os }})
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
os: [macos-latest, ubuntu-latest]
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install Macaulay2 1.26 (latest stable)
|
|
20
|
+
if: runner.os == 'macOS'
|
|
21
|
+
run: |
|
|
22
|
+
brew tap Macaulay2/tap
|
|
23
|
+
brew trust --tap Macaulay2/tap 2>/dev/null || true # newer Homebrew requires explicit tap trust
|
|
24
|
+
brew install macaulay2
|
|
25
|
+
M2 --version
|
|
26
|
+
|
|
27
|
+
- name: Install Macaulay2 1.26 (latest stable)
|
|
28
|
+
if: runner.os == 'Linux'
|
|
29
|
+
run: |
|
|
30
|
+
sudo add-apt-repository -y ppa:macaulay2/macaulay2
|
|
31
|
+
sudo apt-get update
|
|
32
|
+
sudo apt-get install -y macaulay2
|
|
33
|
+
|
|
34
|
+
- name: Install uv
|
|
35
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
36
|
+
shell: bash
|
|
37
|
+
|
|
38
|
+
- name: Show M2 version
|
|
39
|
+
run: |
|
|
40
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
41
|
+
M2 --version
|
|
42
|
+
uv run macaulay2-mcp selftest
|
|
43
|
+
|
|
44
|
+
- name: Install dependencies
|
|
45
|
+
run: |
|
|
46
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
47
|
+
uv sync
|
|
48
|
+
|
|
49
|
+
- name: Lint
|
|
50
|
+
run: |
|
|
51
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
52
|
+
uv run ruff check src tests
|
|
53
|
+
|
|
54
|
+
- name: Test
|
|
55
|
+
run: |
|
|
56
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
57
|
+
uv run pytest -q
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (vX.Y.Z) is pushed, using PyPI
|
|
4
|
+
# TRUSTED PUBLISHING (OpenID Connect) — no API token stored anywhere.
|
|
5
|
+
# One-time setup: https://docs.pypi.org/trusted-publishers/
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- "v*"
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Install uv
|
|
18
|
+
run: curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
19
|
+
|
|
20
|
+
- name: Build sdist + wheel
|
|
21
|
+
run: |
|
|
22
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
23
|
+
uv build
|
|
24
|
+
|
|
25
|
+
- uses: actions/upload-artifact@v4
|
|
26
|
+
with:
|
|
27
|
+
name: dist
|
|
28
|
+
path: dist/
|
|
29
|
+
|
|
30
|
+
publish:
|
|
31
|
+
needs: build
|
|
32
|
+
runs-on: ubuntu-latest
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
id-token: write # required for PyPI trusted publishing (OIDC token minting)
|
|
36
|
+
environment: pypi # PyPI trusted publisher is bound to this environment
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/download-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
- name: Publish to PyPI (trusted publishing, OIDC)
|
|
44
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
dist/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.egg-info/
|
|
6
|
+
.eggs/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
htmlcov/
|
|
13
|
+
|
|
14
|
+
# uv
|
|
15
|
+
uv.lock.bak
|
|
16
|
+
|
|
17
|
+
# Editors / OS
|
|
18
|
+
.vscode/
|
|
19
|
+
.idea/
|
|
20
|
+
.DS_Store
|
|
21
|
+
|
|
22
|
+
# Local secrets / env
|
|
23
|
+
.env
|
|
24
|
+
|
|
25
|
+
# Local (developer) opencode config; the published form lives in the README
|
|
26
|
+
opencode.json
|
|
27
|
+
|
|
28
|
+
# Generated E2E outputs
|
|
29
|
+
e2e/results/
|
|
30
|
+
|
|
31
|
+
# Private contributor/review notes (not part of the published repo)
|
|
32
|
+
notes/
|
|
33
|
+
|
|
34
|
+
# journal artifacts (local audit trail)
|
|
35
|
+
.m2-mcp/
|
|
36
|
+
|
|
37
|
+
# private publishing runbook (kept on disk, stripped from history by filter-repo)
|
|
38
|
+
PUBLISHING.md
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# AGENTS.md — working on macaulay2-mcp
|
|
2
|
+
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
5
|
+
An MCP (Model Context Protocol) server exposing a persistent Macaulay2 1.26
|
|
6
|
+
session as tools. Python, `uv`-managed. Entry point: `macaulay2-mcp`
|
|
7
|
+
(console script) → `src/macaulay2_mcp/cli.py`.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv sync # install/refresh deps
|
|
13
|
+
uv run pytest -q # full test suite (needs M2 1.26 installed)
|
|
14
|
+
uv run ruff check src tests # lint
|
|
15
|
+
uv run macaulay2-mcp selftest# one-command install verification
|
|
16
|
+
uv run macaulay2-mcp # run the MCP server on stdio
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Hard constraints (do not break)
|
|
20
|
+
|
|
21
|
+
1. **Stdio discipline:** when running as an MCP server, stdout carries the
|
|
22
|
+
JSON-RPC protocol. NEVER print to stdout from server code — logging goes
|
|
23
|
+
to stderr (`logging` is already configured that way). `selftest` may use
|
|
24
|
+
stdout (it is not a server).
|
|
25
|
+
2. **M2 protocol — marker handshake:** M2 in pipe mode prints NO bare prompt
|
|
26
|
+
while stdin stays open; it prints `iN : <input>` only together with the
|
|
27
|
+
next input. M2's errors go to its **stderr**, which we merge into stdout
|
|
28
|
+
at the OS level (`stderr=STDOUT`) so error text keeps true stream order
|
|
29
|
+
in the returned output — do NOT separate them again (async drains race).
|
|
30
|
+
Completion of an evaluation is detected by the void marker in `kernel.py`
|
|
31
|
+
(`scan({}, i -> m2MCP<12hex>)`: read until a line ending with the unique
|
|
32
|
+
marker text). The marker must be a COMPLETE statement (blank/comment
|
|
33
|
+
lines dangle and absorb the next line) AND produce no `oN` output —
|
|
34
|
+
an assignment marker polluted M2's `oo`/`ooo` history, breaking the
|
|
35
|
+
official tutorial's `4*5; oo` workflow (regression test:
|
|
36
|
+
`test_oo_history_survives_calls`). Blank or comment-only lines do NOT
|
|
37
|
+
terminate a logical M2 input. SIGINT (m2_interrupt) works in pipe mode:
|
|
38
|
+
M2 prints `error: interrupted`, prompt indices stay in sync, the
|
|
39
|
+
buffered marker still executes (the in-flight evaluate() returns
|
|
40
|
+
normally), and state survives. SIGINT while idle only emits a bare
|
|
41
|
+
`iN :` line — filtered from evaluation blocks.
|
|
42
|
+
`M2Session.interrupt()` is LOCK-FREE by design (evaluate() holds the
|
|
43
|
+
session lock while running); never make it take the lock.
|
|
44
|
+
3. **Golden outputs:** `tests/data/golden.jsonl` pins observable M2 1.26
|
|
45
|
+
behaviour (rendering quirks included). If M2 output changes, review the
|
|
46
|
+
dataset deliberately instead of letting it rot.
|
|
47
|
+
4. **Version pin:** v0.1 supports M2 1.26.x only (`config.py`). Do not add
|
|
48
|
+
version branches; that's a later-version item.
|
|
49
|
+
5. **Timeouts are author-set safety limits** (default 120s, max 3600s).
|
|
50
|
+
Timeout messages must keep saying it is NOT an M2 error and that the
|
|
51
|
+
session was restarted (state lost → retry self-contained).
|
|
52
|
+
6. **Concurrency semantics are by design:** `m2_evaluate` serializes on the
|
|
53
|
+
session lock (one shared kernel = consistent state; concurrent requests
|
|
54
|
+
from subagents are safe, just queued), while `m2_run_script` takes no
|
|
55
|
+
lock (each job is an independent M2 process — that IS the parallelism
|
|
56
|
+
story until the planned job pool). Never "optimize away" the serialization.
|
|
57
|
+
Tests and docs pin outputs, NEVER timings.
|
|
58
|
+
7. **No new user-facing config knobs.** The complete v0.1 set is `M2_BIN`
|
|
59
|
+
(binary location), `MACAULAY2_MCP_JOURNAL` (journal dir / `off`), and
|
|
60
|
+
`MACAULAY2_MCP_OS_ALLOW` (gate unblock list). Pinned flags and limits
|
|
61
|
+
live in `config.py` (`M2_KERNEL_FLAGS`, timeouts).
|
|
62
|
+
8. **Messages inform, never direct.** Every user-facing string (install
|
|
63
|
+
hints, errors, tool outputs, README) states what the suggested action
|
|
64
|
+
does and whether it is reversible — and how to undo it. We do not tell
|
|
65
|
+
users to blindly agree/answer yes; we explain the decision and leave it
|
|
66
|
+
to them. Applies equally to LLM-facing text (INSTRUCTIONS, docstrings):
|
|
67
|
+
the assistant relays information, not pressure.
|
|
68
|
+
9. **Error-continuation semantics are a pinned contract.** M2 continues
|
|
69
|
+
running inputs after an error (no rollback; even mid-input side effects
|
|
70
|
+
persist) — golden cases `error_then_continue`/`partial_input_effect` pin
|
|
71
|
+
this. The continue/restart/inspect options NOTE is composed ONLY in the
|
|
72
|
+
`m2_evaluate` handler for runtime errors (never for interrupted,
|
|
73
|
+
restart, or timeout results, which have their own messages).
|
|
74
|
+
`stop_on_error` uses `split_logical_inputs` (blank/comment lines attach
|
|
75
|
+
forward; trailing uncompletable lines dropped; documented limitation:
|
|
76
|
+
dangling trailing operators). `_marker_handshake` matches the marker
|
|
77
|
+
TEXT anywhere in a line and tracks the index from any `iN :` line —
|
|
78
|
+
do NOT re-anchor to `^iN : <marker>`: a code block ending in a comment
|
|
79
|
+
absorbs the marker as a continuation line and the handshake would hang
|
|
80
|
+
to timeout (regression test: `test_trailing_comment_does_not_swallow_marker`).
|
|
81
|
+
10. **Gate + journal contracts.** The OS-call gatekeeper (`gatekeep.py`)
|
|
82
|
+
refuses process/file/network/env/session symbols BEFORE sending anything,
|
|
83
|
+
at all four entry points (evaluate, run_script, import_file,
|
|
84
|
+
load_package path check); it matches on `scanner.mask()` output —
|
|
85
|
+
strings/comments never false-positive; `value()` is deliberately NOT
|
|
86
|
+
blocked (documented bypass; friction not sandbox). The journal
|
|
87
|
+
(`journal.py`) defaults ON at `./.m2-mcp/`, writes lazily so the header
|
|
88
|
+
carries clientInfo (via `ctx.request_context.session.client_params` —
|
|
89
|
+
SDK v2 stdio path), truncates fields at 1 MiB, and MUST NEVER raise into
|
|
90
|
+
a tool call (self-disables with one stderr warning) and never touches
|
|
91
|
+
stdout. Tool handlers take a hidden `ctx: Context = None` param — the SDK
|
|
92
|
+
excludes it from the JSON schema; keep that pattern.
|
|
93
|
+
|
|
94
|
+
## Layout
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
src/macaulay2_mcp/
|
|
98
|
+
config.py binary discovery, version gate, pinned flags/timeouts
|
|
99
|
+
scanner.py offset-preserving string/comment mask (shared by kernel+gate)
|
|
100
|
+
kernel.py M2Session (persistent kernel), M2ScriptRunner (batch), messages
|
|
101
|
+
gatekeep.py OS-call blocklist + rejection messages (mask-based matching)
|
|
102
|
+
journal.py JSONL audit journal (lazy header w/ clientInfo, never raises)
|
|
103
|
+
server.py the 8 MCP tools + INSTRUCTIONS (LLM-facing, keep accurate)
|
|
104
|
+
cli.py entry point: server mode | selftest | --version
|
|
105
|
+
tests/
|
|
106
|
+
test_kernel.py protocol tests (skip if M2 1.26 missing)
|
|
107
|
+
test_gatekeep.py masking / enforcement / live blocking
|
|
108
|
+
test_journal.py journal units + live clientInfo-in-header round trip
|
|
109
|
+
test_mcp_client.py client-level tests over stdio
|
|
110
|
+
e2e/ Docker + Ollama + opencode demo (opt-in: run_e2e.sh)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## M2 1.26 idioms relevant to this codebase
|
|
114
|
+
|
|
115
|
+
* `gb I` returns a GroebnerBasis; see polynomials via `print generators (gb I)`.
|
|
116
|
+
* A trailing `;` SUPPRESSES a statement's result display (`betti G;` prints
|
|
117
|
+
nothing); `A; B` on one line shows only B. Use newlines + explicit `print`.
|
|
118
|
+
* Family loops: `for k from 1 to n list (J := ideal(...); <expr>)` with `:=`
|
|
119
|
+
for per-iteration locals. `I_k = ...` is ONE symbol named "I_k", not
|
|
120
|
+
indexing. `print (a | b)` needs parens — `print a | b` is `(print a) | b`.
|
|
121
|
+
* M2 strings use double quotes; single quotes are invalid.
|
|
122
|
+
* `unloadPackage` and `importFile` do not exist in 1.26 — package "unload" =
|
|
123
|
+
session reset; file import = evaluate the file's contents (m2_import_file).
|
|
124
|
+
* Preloaded packages error with "not reloaded; try Reload => true";
|
|
125
|
+
`m2_load_package` turns that into an "already loaded" note and NEVER
|
|
126
|
+
force-reloads — M2's own reload machinery breaks on packages whose source
|
|
127
|
+
has dependency `needsPackage` lines (verified with PrimaryDecomposition).
|
|
128
|
+
`reload=true` is the caller's explicit opt-in for that fragile path.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
title: "macaulay2-mcp: a Model Context Protocol server for a persistent Macaulay2 session"
|
|
4
|
+
authors:
|
|
5
|
+
- family-names: Kim
|
|
6
|
+
given-names: Youngsu
|
|
7
|
+
repository-code: https://github.com/youngsu-Kim/macaulay2-mcp
|
|
8
|
+
license: GPL-3.0-or-later
|
|
9
|
+
version: 0.1.0
|
|
10
|
+
date-released: 2026-09-06
|