jflap-automata
by gkavin2527
README.md
# JFLAP Automata Assistant — MCP Server
Claude is the reasoning layer (classifies the problem, designs states/transitions,
explains results). This server is the execution layer: it builds the formal
machine, validates it independently, serializes it to a JFLAP `.jff` file, and
simulates test strings with real nondeterministic search — Claude never
fabricates acceptance/rejection results.
**This README covers the overview and setup.** For the complete 33-tool
reference, resource limits, memory lifecycle, the GUI automation
investigation, and project history, see
[`DOCUMENTATION.md`](DOCUMENTATION.md).
## What's implemented
| Component | Status |
|---|---|
| MCP server + tool discovery (33 tools) | done |
| PDA builder, validator, simulator (nondeterministic BFS), JFLAP export | done |
| DFA/NFA builder, validator, simulator (subset construction), JFLAP export | done |
| Turing Machine builder, validator, tape simulator, JFLAP export | done |
| TM reject-state export correctness (no outgoing transitions, never `<final/>`) | done |
| CFG builder, validator, epsilon/unit/useless transformations, bounded leftmost-derivation | done |
| CFG **exact** membership via Earley parsing | done |
| JFLAP GUI automation: process control, menu navigation, file opening | **real-GUI-verified** — see [`DOCUMENTATION.md`](DOCUMENTATION.md) |
| JFLAP GUI automation: simulation input entry | **confirmed impossible on this Java/macOS bridge** — Python simulators remain authoritative |
| AI-powered Automata Workbench: `solve_automata_problem` orchestration, readable transition tables | done |
| Bounded, LRU-evicting automaton/grammar storage + 6 management tools | done |
| Server-enforced resource limits (every search parameter has a hard maximum) | done |
| AppleScript-injection-hardened GUI automation, verified against the real interpreter | done |
373/373 pytest tests pass. Most call the underlying builder/validator/
simulator/storage functions directly (in-process, no MCP transport) —
the fastest way to cover algorithmic correctness. A separate, genuinely
real check also exists: `tests/test_mcp_stdio.py` spawns `server.py` as
an actual subprocess and drives it over the SDK's real stdio transport
(`stdio_client` + `ClientSession`) for 3 representative tools
(`create_fa`+`simulate_automaton`, `create_cfg`+`check_cfg_membership`,
plus a full `list_tools()` call asserting all 6 of its spot-checked tools
appear) — this is deliberately a protocol-wiring/schema check, not a
claim that every one of the 33 tools has been exercised over the wire;
the in-process tests are what cover each tool's actual behavior.
JFLAP 7.1 is installed on this machine and has been driven for real —
launched, closed gracefully, had files opened into it via the MCP server
with the result visually confirmed (title bar + canvas matching the
generated automaton exactly), and had its menus navigated through real
Accessibility calls. Automated *simulation input entry* was investigated
via five independent real techniques and confirmed — not assumed — to be
unsupported by this specific Java/AWT-to-Cocoa bridge. The project's
settled design: Python simulators stay the sole source of truth for
results; JFLAP is for visual display and optional manual verification.
See [`DOCUMENTATION.md`](DOCUMENTATION.md)'s "GUI automation" section for
the full investigation and architecture.
`storage/generated/*.jff` holds every generated file; open it from JFLAP via
File > Open, or ask Claude to call `open_in_jflap` (best-effort `open -a JFLAP`,
falls back to manual instructions if JFLAP.app isn't installed).
## Architecture
At its simplest, this is a straight pipeline:
```
Claude / MCP client
|
v
MCP server (server.py)
|
v
Builders (builders/*.py -- construct the Automaton/Grammar)
|
v
Validators (validators/*.py -- independent structural checks)
|
v
Simulators / CFG parser
(simulators/*.py -- fa/pda/tm_simulator.py, cfg_processor.py, earley_parser.py)
|
v
JFF generator (generators/jff_generator.py -- Automaton/Grammar -> JFLAP .jff XML)
|
v
Optional JFLAP desktop integration (gui/*.py -- launch, open, screenshot, close)
```
**Python is the authoritative simulation engine; JFLAP is an optional
visual companion.** Every ACCEPTED/REJECTED/MEMBER result a caller
receives comes from the Simulators/CFG-parser step above — never from
JFLAP. The last step (JFLAP desktop integration) is entirely optional:
nothing above it depends on JFLAP being installed, and nothing in it can
override or substitute for a Python-computed result. The fuller diagram
below shows the same pipeline with the branching construction paths
(DFA/NFA/PDA/TM/CFG) and the two in-memory stores (automata, grammars)
made explicit:
```mermaid
flowchart TD
User["User"] --> Claude["Claude\n(classifies the problem, designs states/transitions, explains results)"]
Claude --> MCPClient["MCP Client\n(Claude Desktop / Claude Code)"]
MCPClient -->|stdio, MCP protocol| Server["MCP Server -- server.py\n(33 tools)"]
Server --> Orchestration["Construction & orchestration\n(create_*, solve_automata_problem)"]
Orchestration --> DFA["DFA / NFA\nbuilders/fa_builder.py"]
Orchestration --> PDA["PDA\nbuilders/pda_builder.py"]
Orchestration --> TM["Turing Machine\nbuilders/tm_builder.py"]
Orchestration --> CFG["CFG\nbuilders/cfg_builder.py"]
DFA --> Validators["Validators\nvalidators/*.py\n(independent structural checks)"]
PDA --> Validators
TM --> Validators
CFG --> Validators
Validators --> Simulators["Python Simulators -- AUTHORITATIVE EXECUTION\nsimulators/*.py\n(fa/pda/tm_simulator.py, cfg_processor.py, earley_parser.py)"]
Simulators --> JFF["JFLAP .jff export\ngenerators/jff_generator.py"]
JFF --> GUI["JFLAP 7.1 GUI integration -- gui/*.py\n(launch, open, screenshot, close -- SUPPORTED, non-authoritative)"]
Simulators -.->|"ACCEPTED / REJECTED / MEMBER\n(always Python-verified)"| Server
GUI -.->|"visual confirmation only\n(never a simulation result)"| Server
Server --> MCPClient
style Simulators fill:#2d6a4f,color:#fff
style GUI fill:#555,color:#fff
```
**The authority boundary, explicitly**: every ACCEPTED/REJECTED/MEMBER
result Claude reports comes from the Python simulators (green above),
never from JFLAP. JFLAP's GUI integration (gray above) is a separate,
genuinely useful but non-authoritative layer — launching the app,
opening a generated `.jff` file so a human can see the automaton, and
taking a screenshot. `run_jflap_simulation` deliberately reports
`NOT_SUPPORTED` rather than a guessed result, because typing a test
string into JFLAP's own dialogs cannot be automated on this platform
(see [`DOCUMENTATION.md`](DOCUMENTATION.md)'s "Known limitations" and
"GUI automation" sections) — the Python simulators are never bypassed,
with or without JFLAP installed.
## Screenshots
All real captures from the actual running application — none are
fabricated or AI-generated. See [`docs/screenshots/README.md`](docs/screenshots/README.md)
for how each one was captured and verified. Captured before the storage-
lifecycle tools were added, so the `/mcp` panel screenshot below shows 27
tools, not today's 33 — the panel itself is still genuine, just from an
earlier point in the project.
| | |
|---|---|
|  <br> A generated DFA ("ends in 01") open in JFLAP 7.1 |  <br> The canonical `a^(2n)b^n` PDA, stack operations visible |
|  <br> The "even number of 1s" TM in JFLAP |  <br> A generated CFG in JFLAP's grammar table view |
|  <br> `jflap-automata` connected in Claude Code's `/mcp` panel, 27 tools |  <br> Real terminal output: DFA/PDA/TM construction, validation, and simulation results |
|  <br> Real terminal output: the compact TM trace mode and step reconstruction | |
## End-to-End Demonstration
[`examples/end_to_end_demo.py`](examples/end_to_end_demo.py) is a
standalone script that exercises the project's actual DFA/PDA/TM/CFG
builders, validators, simulators, and JFLAP exporters directly — no
Claude, no MCP protocol, no network, no API keys. It's the fastest way
to confirm the whole non-GUI pipeline works on your machine.
```bash
python examples/end_to_end_demo.py
```
### What it does
Six stages, each printing its own status: construct a DFA ("ends in
01") and simulate 4 test strings; construct the project's canonical
`a^(2n)b^n` PDA and simulate an accepted/rejected pair; construct a
small "even number of 1s" Turing Machine and report its final
configuration; construct a small CFG and check membership of one member
and one non-member string via the exact Earley parser
(`check_cfg_membership` uses); re-run the TM with
`trace_format="compact"` and reconstruct its initial and final
configurations; and finally export all four as `.jff` files.
### Example output
```
==========================================================
JFLAP MCP -- AUTOMATA WORKBENCH DEMO
==========================================================
[1/6] DFA CONSTRUCTION
Status: SUCCESS
Validation: VALID
Input '1101': ACCEPTED
Input '1010': REJECTED
Input '1111': REJECTED
Input '01': ACCEPTED
[2/6] PDA CONSTRUCTION
Status: SUCCESS
Validation: VALID
Input 'aab': ACCEPTED
Input 'aabb': REJECTED
...
[6/6] JFLAP EXPORT
DFA -> .../storage/generated/demo/demo_dfa.jff
...
```
(Full output is reproducible by running the command yourself — see
`docs/screenshots/06-automata-workflow.png` and
`07-tm-compact-trace.png` for real captures of this exact script's
output.)
### Optional arguments
| Flag | Effect |
|---|---|
| `--skip-jflap` | Skip JFLAP entirely, including the export stage's JFLAP-open attempt. |
| `--open-jflap` | Open the generated DFA in a real JFLAP window after export (requires Java + a configured `JFLAP.jar`, see "Setup" below). |
| `--output-dir PATH` | Where generated `.jff` files are written (default: `storage/generated/demo/`). |
Run `python examples/end_to_end_demo.py --help` for the same, from the
script itself.
### Requirements
Only this project's existing virtual environment
(`pip install -r requirements.txt`) — no JFLAP, no Claude, and no
network access are required for the default run. `--open-jflap` is the
only thing that needs JFLAP/Java set up.
### What this demo proves and doesn't
Proves the DFA/PDA/TM/CFG builders, validators, and simulators work
correctly end-to-end, independent of Claude or MCP, that `.jff` export
works for all four automaton/grammar types, and that the TM compact-
trace architecture and `reconstruct_at_step` work as documented. Does
**not** exercise the MCP protocol itself (see `tests/test_mcp_stdio.py`
for that) or demonstrate JFLAP GUI *simulation input*, which remains
unsupported on this platform regardless of what this script does.
## Project layout
```
jflap-mcp/
├── server.py # MCP tool definitions, wires everything together
├── models.py # shared State/Transition/Automaton + Grammar/Production dataclasses
├── resource_limits.py # per-operation default + hard-maximum resource limits, enforced server-side
├── storage.py # BoundedStore: thread-safe, LRU-evicting, pin-aware automaton/grammar storage
├── reporting.py # transition-table/production-list/summary formatters (pure, no new logic)
├── builders/
│ ├── common.py # shared State-construction helper (build_states) used by all three builders below
│ ├── pda_builder.py
│ ├── fa_builder.py # DFA + NFA
│ ├── tm_builder.py
│ └── cfg_builder.py
├── validators/
│ ├── common.py # shared single-character-symbol check used by both validators below
│ ├── automata_validator.py # structural + semantic checks for DFA/NFA/PDA/TM, optional test_cases cross-check
│ └── cfg_validator.py # structural checks for Grammar objects
├── generators/jff_generator.py # Automaton -> JFLAP .jff XML; Grammar -> JFLAP grammar .jff XML
├── simulators/
│ ├── pda_simulator.py # nondeterministic BFS over (state, input, stack) configs
│ ├── fa_simulator.py # epsilon-closure subset simulation
│ ├── tm_simulator.py # tape/head simulation, halt-on-no-transition semantics
│ ├── cfg_processor.py # nullable-set/epsilon-removal, unit-production elimination,
│ │ # useless-symbol removal, bounded leftmost-derivation search
│ └── earley_parser.py # exact CFG membership: Predictor/Scanner/Completer chart parser
├── jflap/simple_launcher.py # minimal file-based open: best-effort `open -a JFLAP <file>` (renamed
│ # from jflap_launcher.py to avoid a naming collision with gui/jflap_launcher.py)
├── gui/ # real JFLAP desktop automation (separate from simulators/)
│ ├── jflap_launcher.py # locate jar (env var/config/search, never hardcoded), launch via java -jar
│ ├── jflap_automation.py # AppleScript/Accessibility menu + keystroke automation; _applescript_quote
│ │ # renders any non-printable-ASCII character as `character id <n>`, never
│ │ # raw-interpolated, so a crafted value can't inject additional AppleScript
│ ├── screenshot_manager.py # `screencapture`-based, unique non-overwriting filenames
│ ├── jflap_controller.py # orchestrates the three above; the only module server.py imports from gui/;
│ │ # also holds GUI_TOOL_CAPABILITIES, the structured FULL/PARTIAL matrix
│ ├── verification.py # Python-vs-JFLAP comparison: MATCH/MISMATCH/INCONCLUSIVE/JFLAP_UNAVAILABLE
│ └── jflap_config.json # optional: {"jar_path": "/full/path/to/JFLAP.jar"} (not checked in; user-provided)
├── storage/generated/ # output .jff files land here (gitignored, recursive)
├── outputs/screenshots/ # GUI automation screenshots land here (gitignored, recursive; unique filenames, never overwritten)
├── examples/end_to_end_demo.py # standalone demo: no Claude, no MCP, no network -- see "End-to-End Demonstration"
├── docs/screenshots/ # real application screenshots for README.md -- see docs/screenshots/README.md
├── pyproject.toml / requirements.txt / uv.lock # see "Setup" -- pip and uv are both supported, kept in sync
├── DOCUMENTATION.md # full tool reference, architecture deep-dive, GUI investigation, project history
└── tests/ # pytest suite (373 tests, all passing) + bench_tm_trace.py
```
## Setup
Two supported install paths — pick one. Both install the exact same
dependency versions (`uv.lock` and `requirements.txt` are kept in sync;
see "Dependencies" below for the verified version range).
**Option A: pip**
```bash
git clone <this-repo-url> jflap-mcp
cd jflap-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python3 -m pytest tests/ -q # sanity check
```
**Option B: uv** (faster, reproducible via the committed lockfile)
```bash
git clone <this-repo-url> jflap-mcp
cd jflap-mcp
uv sync --extra dev # creates .venv, installs runtime + pytest from the lockfile
uv run pytest tests/ -q # sanity check
```
`--extra dev` is required for the sanity-check command specifically —
`pytest` is declared as an optional (`dev`) dependency, so a plain
`uv sync` (confirmed by testing both) installs only the runtime
dependency (`mcp[cli]`) and intentionally skips it, matching what an end
user running the server (not its tests) actually needs. (Install `uv`
itself first if you don't have it: `pip install uv`, or see
https://docs.astral.sh/uv/getting-started/installation/.) Regenerate the
lockfile with `uv lock` after any dependency change in `pyproject.toml`.
Requires **Python 3.10+** (the codebase uses `X | None` union-type
annotations, e.g. in `simulators/tm_simulator.py`, which need 3.10 or
later). Tested on Python 3.10 and 3.14 by actually running the full test
suite on both — not assumed from the `requires-python` declaration alone
(see [`DOCUMENTATION.md`](DOCUMENTATION.md)'s "Known limitations" for the
exact versions tested). No OS-specific code runs in this path — the
Python simulators, builders, validators, and MCP server all work
identically on macOS, Linux, and Windows. GUI automation (the next
section) is **macOS-only** and has not been tested on other platforms —
see "Platform support" below.
### Optional: enabling real JFLAP GUI automation
The Python simulators and MCP tools all work with zero setup. GUI
automation (opening files in JFLAP, screenshots, visual comparison)
additionally needs, all currently satisfied on this machine:
1. **Java** — `openjdk 26.0.2` via Homebrew. Check with `java -version`.
2. **JFLAP.jar** — download from https://www.jflap.org, then either:
- `export JFLAP_JAR_PATH=/full/path/to/JFLAP.jar`, or
- write `{"jar_path": "/full/path/to/JFLAP.jar"}` to `gui/jflap_config.json`.
Nothing here guesses a path that wasn't actually verified to exist —
`locate_jflap_jar()` checks a short list of conventional locations
(`~/JFLAP/`, `~/Downloads/`, `/Applications/`, …) but only ever returns
one that's confirmed on disk, never an assumption.
3. **macOS Automation permission** for whatever process runs `server.py`
— System Settings > Privacy & Security > Automation, allow it to
control "System Events".
4. **macOS Accessibility permission** for the same process — System
Settings > Privacy & Security > Accessibility. Separate from
Automation; required for menu clicks and window inspection.
5. **macOS Screen Recording permission** for the same process — System
Settings > Privacy & Security > Screen Recording. Required for
`capture_jflap_screenshot`.
Call `get_jflap_environment_status` (an MCP tool, or
`gui.jflap_controller.get_environment_status()` directly) at any time to
see exactly which of these are satisfied right now — it reports Java,
the jar path, the running JFLAP process (if any), and both Automation and
Accessibility permission status independently (one can be granted without
the other).
**Not covered by any of the above**: automated entry of a test string
into JFLAP's own simulation dialogs. This was investigated thoroughly
(five independent techniques against a real running JFLAP 7.1) and
confirmed to be a structural limitation of this Java/macOS Accessibility
bridge, not a missing permission — see [`DOCUMENTATION.md`](DOCUMENTATION.md)'s
"GUI automation" section for the full investigation and why the Python
simulators remain the authoritative source of simulation results
regardless.
### Dependencies
`mcp[cli]>=2.0,<3.0` is the only runtime dependency, and this range is
verified, not assumed: `mcp` 1.x exposed a completely different API
(`FastMCP` from `mcp.server.fastmcp`); 2.x renamed it to `MCPServer`
(`from mcp.server.mcpserver import MCPServer`) and moved `ToolError` to
`mcp.server.mcpserver.exceptions` — the exact imports `server.py` uses.
Installing `mcp` 1.x and importing `server.py` fails immediately with an
error message from the SDK itself confirming the rename. The lower bound
(`>=2.0`) was confirmed by installing `mcp==2.0.0` (the first stable 2.x
release) in an isolated environment and running this project's entire
pytest suite — including the real, non-mocked MCP stdio-protocol tests in
`tests/test_mcp_stdio.py` — against it; everything passed, and the same
suite was separately run against `mcp==2.3.0` (what this project's own
`.venv` actually uses). Neither claim extends to every 2.x point release
in between — only these two were actually tested. `<3.0` guards against
a future rename of the same kind; no `mcp` 3.x exists yet.
## Claude Desktop configuration
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` and
add (merge, don't replace, if the file already has other servers).
Replace `/absolute/path/to/jflap-mcp` with where you actually cloned this
repo — both `command` and the arg must be absolute paths, not `~` or a
relative path:
```json
{
"mcpServers": {
"jflap-automata": {
"command": "/absolute/path/to/jflap-mcp/.venv/bin/python3",
"args": [
"/absolute/path/to/jflap-mcp/server.py"
]
}
}
}
```
### Claude Code / VS Code configuration
Claude Code reads the same shape from a project-local `.mcp.json` at the
repo root (this file is gitignored — it's machine-specific, so each
developer creates their own):
```json
{
"mcpServers": {
"jflap-automata": {
"type": "stdio",
"command": "/absolute/path/to/jflap-mcp/.venv/bin/python3",
"args": ["/absolute/path/to/jflap-mcp/server.py"],
"env": {
"JFLAP_JAR_PATH": "/absolute/path/to/JFLAP.jar",
"PYTHONUNBUFFERED": "1"
}
}
}
}
```
`JFLAP_JAR_PATH` and `PYTHONUNBUFFERED` are optional — omit the `env`
block entirely if you haven't set up JFLAP GUI automation yet (see
above); the Python simulators and every non-GUI tool work without it.
After editing either file, fully restart Claude Desktop / reload the
Claude Code window, then verify the connection: open Settings >
Developer/Extensions (Desktop) or run `/mcp` (Claude Code) and confirm
`jflap-automata` shows as connected with 33 tools listed.
### Verifying the server on its own (without Claude)
Before wiring it into Claude at all, confirm the server itself starts
cleanly and the tool list is what you expect:
```bash
# Syntax/import check -- should produce no output on success
.venv/bin/python -c "import server"
# Full MCP tool listing over the real stdio protocol
.venv/bin/python -c "
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
async def main():
params = StdioServerParameters(command='.venv/bin/python3', args=['server.py'])
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print(len(tools.tools), 'tools:', sorted(t.name for t in tools.tools))
asyncio.run(main())
"
```
The 33 tools you should see listed (full reference in
[`DOCUMENTATION.md`](DOCUMENTATION.md)):
- Generic (work across DFA/NFA/PDA/TM): `classify_automata`,
`validate_automaton`, `generate_jff`, `simulate_automaton`,
`get_execution_trace`, `open_in_jflap`
- Construction: `create_pda`, `create_fa`, `create_tm`, `create_cfg`
- TM-specific: `validate_tm`, `simulate_tm`, `generate_tm_jff`
- CFG-specific: `validate_cfg`, `eliminate_unit_productions`, `derive_cfg`,
`check_cfg_membership`, `generate_cfg_jff`
- GUI automation: `get_jflap_environment_status`, `launch_jflap`,
`close_jflap`, `open_jff_file`, `run_jflap_simulation`,
`capture_jflap_screenshot`, `compare_python_jflap`
- Workbench orchestration: `solve_automata_problem`, `get_transition_table`
- Storage lifecycle management: `list_automata`, `list_grammars`,
`delete_automaton`, `delete_grammar`, `clear_automata`, `clear_grammars`
`validate_automaton`, `generate_jff`, `simulate_automaton`, and
`get_execution_trace` all dispatch on the stored automaton's type, so they
work for TM automatons too — the TM-specific tools exist alongside them
for discoverability. There is no generic equivalent for CFG tools since a
`Grammar` isn't a states-and-transitions automaton; it has its own tools
and its own in-memory store (`_GRAMMARS`, separate from `_AUTOMATA`).
`check_cfg_membership` and `get_jflap_environment_status` weren't in the
originally suggested tool lists — the first because without it Claude had
no way to get a conclusive CFG membership answer through MCP at all (only
a bounded search), the second because every other GUI tool needs that
environment information to answer honestly rather than guessing.
`solve_automata_problem`/`get_transition_table` are pure
orchestration/formatting over all of the above — see
[`DOCUMENTATION.md`](DOCUMENTATION.md)'s "AI-Powered Automata Workbench"
section. The six storage-lifecycle tools exist because `_AUTOMATA`/
`_GRAMMARS` are now bounded and LRU-evicting (see
[`DOCUMENTATION.md`](DOCUMENTATION.md)'s "Memory lifecycle" section) —
they let a caller see what's stored and free space deliberately instead
of only waiting for automatic eviction.
## Platform support
| Component | macOS | Linux | Windows |
|---|---|---|---|
| Builders, validators, Python simulators, MCP server | Tested, works | Not tested — no OS-specific code in this path, likely works | Not tested — no OS-specific code in this path, likely works |
| JFLAP GUI automation (`gui/`) | Tested, works | Not supported — `gui/jflap_automation.py` shells out to macOS-only `osascript`/System Events | Not supported — same reason |
| File-based JFLAP open (`open_in_jflap`) | Tested, works (`open -a JFLAP`) | Not supported — `open` is macOS-specific | Not supported — same reason |
If you're on Linux or Windows: every construction/validation/simulation/
`.jff`-export tool works identically (they're pure Python with no
platform-specific calls); only the `gui/` automation layer and
`open_in_jflap` are macOS-only today. You can still open a generated
`.jff` file in JFLAP manually on any platform JFLAP itself supports.
## Full documentation
[`DOCUMENTATION.md`](DOCUMENTATION.md) has everything not covered above:
the complete 33-tool reference (every parameter, return shape, and error
status), the resource-limits table (every default and server-enforced
hard maximum), the memory-lifecycle design (bounded storage, LRU
eviction, pinning), the recommended Claude custom-instructions prompt,
the AI-Powered Automata Workbench walkthrough, the PDA/TM/CFG
architecture deep-dives, the full GUI-automation investigation (five
techniques tried, why none work, the capability matrix, mocked-vs-real
tests), the project's 0.1.0 audit history, and the contributing guide.
## Contributing
See [`DOCUMENTATION.md`](DOCUMENTATION.md)'s "Contributing" section for
the full house rules. Short version: run the full test suite first
(`.venv/bin/python -m pytest tests/ -q`, or `uv run pytest tests/ -q`)
and confirm it's green before making changes, add tests for anything you
change, and open a PR describing what changed and why.
## License
[MIT](LICENSE) — Copyright (c) 2026 Kavin.
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for the version history.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo recent community issues