The shortest honest path is the engine on its own: a Python package and a vine CLI, pointed at a folder. Everything else in the project (the Station host, the Studio console, the browser Clipper) sits around that and none of it is required here. A forest on your laptop, served over stdio, is a complete deployment.
Before you start
- Python 3.11 or newer. The package declares
requires-python = ">=3.11". - Git on your PATH. The engine shells out to
git:vine initcreates a repository inside the forest folder, and every write is a commit in it. - Nothing else. The runtime dependencies are the MCP SDK, Pydantic and PyYAML. Entry search is BM25 over SQLite FTS5, which ships with Python, so there is no embedding model to run and no vector database to stand up.
What is optional, and what it buys
The ingest extra adds the .docx, .xlsx and .xls converters. An embedding endpoint enables the Canopy, the optional vector layer. A chat endpoint enables curation, the one stage of ingestion that calls an LLM to write better summaries. Skip all three and the forest still works: it just carries the summaries the Gardener derived mechanically, and ranks entry points lexically.
Install the engine
The engine is installed from the repository. The clone is also where the spec, the paper and the benchmark live, which is useful the first time you want to check what a flag really does.
git clone https://github.com/JimmyWesley/MonkeyLLM.git
cd MonkeyLLM
# The virtualenv is not optional on a modern machine: Homebrew Python
# and Debian/Ubuntu system Python both refuse a bare pip install with
# "error: externally-managed-environment" (PEP 668).
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e . # the monkeyllm package + the vine CLI
vine --help# Adds the .docx, .xlsx and .xls converters
# (python-docx, openpyxl, xlrd). Everything else works without it.
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[ingest]"# The test suite and the Station host, for contributors
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]" && pip install -e apps/station
python -m pytest -qPDFs are deliberately not built in. A good PDF extractor is a heavy and often copyleft dependency, so instead of forcing one on you the Gardener takes a command hook you name yourself. That is the last troubleshooting entry below, and it is two lines of YAML.
Plant a forest and feed it
- 1
Create an empty forest
vine initturns a folder into a forest: a master index, the dialect that declares which node and edge types are legal, a.gitignore, and a git repository with the first commit. The folder is created if it does not exist.--titleis required and becomes the master index heading;--summaryis optional and gets a sensible default.bashvine init --forest ./brain --title "My brain"textinit outputforest created at /home/you/brain (commit 4f1c2a9e) next: vine validate / vine serve, or plant() your first nodes - 2
Feed it a folder of documents
vine adoptmirrors an existing tree into the forest. Every source directory becomes a branch (an index node), every file is converted and planted as its own node with a title, a summary and a stable id derived from its path. It is deterministic and there is no LLM in the loop.bashvine adopt ./my-documents --forest ./brainThe report prints only the buckets that are not empty. Planted, updated, unchanged and stale list node ids; unsupported and errors list source paths, by name, so you can see exactly what did not land.
textadopt outputplanted (3): policies/expense-policy policies/travel handbook/onboarding branches (2): policies/_index handbook/_index unsupported (1): archive/scan-2019.pdfAdding
--curatesends each draft through an OpenAI-compatible chat endpoint (MONKEYLLM_LLM_ENDPOINT) to write the summary, the tags and edge proposals, then rolls branch summaries up from their children. Leave it off for now: you can curate later, and a forest with mechanical summaries is already navigable. - 3
Check the result
vine validatelints every node against the schema: required frontmatter, summary length, resolvable wikilinks, payload hash drift. Add--strictto make warnings fail too, which is what you want in CI.bashvine validate --forest ./braintextvalidate output0 error(s), 0 warning(s)
Your first query
There is no vine query verb: reading a forest is what the primitives are for, and you reach them from your own Python or over MCP. Start in Python, because the round trip is shorter and the response is the same object an agent would receive.
locate is the entry search. It reads only curated metadata (title, aliases, tags, summary), never bodies, and answers with ranked places to start.
Request
from monkeyllm import Vine
vine = Vine("./brain", writable=False)
print(vine.locate("expense policy", k=2))
vine.close()Response
{
"results": [
{
"id": "policies/expense-policy",
"kind": "banana",
"type": "note",
"title": "Expense policy",
"summary": "Reimbursement rules for travel, meals and equipment. Receipts are required above 50 USD and approvals go through the team lead.",
"trail": ["_index", "policies/_index"],
"score": 1.0,
"heat": 0.0
},
{
"id": "policies/_index",
"kind": "branch",
"type": "branch",
"title": "policies",
"summary": "Documents adopted from source folder 'policies'.",
"trail": ["_index"],
"score": 0.6218,
"heat": 0.0,
"coverage": "2 bananas, 0 sub-branches"
}
],
"truncated": false
}Two things in that response are worth naming now. kind is either banana (a leaf that carries knowledge) or branch (a region you can land in and navigate locally), and a branch result carries its coverage. trail is the chain of index nodes from the root down to the result, so an agent always knows where it just landed. heat is pheromone, and it is 0.0 here because nothing has ever been read.
If you would rather get evidence in one call than navigate step by step, harvest is the composite that does it: entry search plus a literal grep of the bodies, fused, with the matched sections attached. It calls no model, on either side.
Request
from monkeyllm import Vine
from monkeyllm.harvest import harvest
vine = Vine("./brain", writable=False)
print(harvest(vine, "expense policy", terms=["receipt"], k=2))
vine.close()Response
{
"query": "expense policy",
"terms": ["receipt"],
"results": [
{
"id": "policies/expense-policy",
"title": "Expense policy",
"type": "note",
"trail": ["_index", "policies/_index"],
"summary": "Reimbursement rules for travel, meals and equipment. Receipts are required above 50 USD and approvals go through the team lead.",
"score": 0.0328,
"found_by": ["locate", "sniff"],
"matches": [
{
"section": "Receipts",
"line": 24,
"snippet": "a receipt is required for any single item above 50 USD"
}
],
"content": [
{
"section": null,
"body": "# Expense policy\n\n## Reimbursable\n...",
"body_tokens": 412
}
]
}
],
"truncated": false
}Open read-only unless you are writing
A writable Vine takes a writer lock (.vine.lock at the forest root): one writer per forest, any number of readers. Passing writable=False keeps queries out of that contention, and vine.close() releases everything the object holds.
Serve it over MCP
The same forest speaks MCP straight from the CLI, with no host and no accounts. The server exposes fourteen tools: forests, locate, sniff, look, move, pick, scan, harvest, view, query, tend, plant, graft and close_session.
Over stdio
The default transport. The MCP client spawns the process and talks to it over pipes, which is what you want for a forest on your own machine.
vine serve --forest ./brain--readonly serves reads only, and refuses plant, graft and tend up front. It also skips the writer lock, so several read-only servers can share one forest.
vine serve --forest ./brain --readonlyOne server can also hold many forests. --root is registry mode, and it is mutually exclusive with --forest.
# Every subdirectory holding an _index.md becomes a servable forest.
# Tools then require forest=<id>, and forests() lists the ids.
vine serve --root ./forestsOver HTTP
Streamable HTTP, for a client that connects to a server you are already running. --host defaults to 127.0.0.1 and --port to 8000.
vine serve --forest ./brain --transport http --host 127.0.0.1 --port 8000
# The MCP endpoint is then http://127.0.0.1:8000/mcpThis transport has no authentication
vine serve is the bare engine. Anyone who can reach the port can read the forest, and write to it unless you passed --readonly. Keep it on loopback. Identity, API keys, per-forest capabilities, scoped grants and an audit trail are what the Station adds on top, and it serves MCP at /mcp with the same contract.
Point an agent at it
Any MCP-capable runtime connects the same way. In stdio mode you do not run vine serve yourself: the client spawns it, so give it an absolute path to the forest.
{
"mcpServers": {
"monkeyllm": {
"command": "vine",
"args": ["serve", "--forest", "/absolute/path/to/brain"]
}
}
}{
"mcpServers": {
"monkeyllm": {
"type": "http",
"url": "http://127.0.0.1:8000/mcp"
}
}
}# stdio: the client spawns the server itself, so you do not run it
claude mcp add monkeyllm -- vine serve --forest /absolute/path/to/brain
# or point at one you are already running over HTTP
claude mcp add --transport http monkeyllm http://127.0.0.1:8000/mcpThe server ships its own instructions, so a connected model already knows the shape of the thing. The moves worth teaching it explicitly on the first session:
look("_index")returns the master branch: the map, the regions and the landmarks. It is the cheapest possible orientation.forests()first, when the server was started with--root. Every other tool then needsforest=<id>.sniff(terms)when the fact is inside a body and no summary would ever mention it: a ticket number, an SKU, a person named once in a paragraph.truncated: truemeans ask narrower, not retry harder. Every read has a token budget and every cut is explicit.
What just happened on disk
Nothing here is a black box, which is most of the point. After vine init the folder holds four things:
brain/
├── .git/ # the forest's own repository, created by init
├── .gitignore # _derived/, .vine.lock, *.db, *.sqlite, _assets/
├── _index.md # the master branch: the map an agent reads first
└── _meta/
└── schema.md # the dialect: which node types and edge types are legalAfter vine adopt, the source tree is mirrored as markdown. Each directory became a branch node, planted before the files under it, and the Gardener recorded where the documents came from:
brain/
├── _index.md
├── _meta/
│ ├── schema.md
│ └── gardener.yaml # source_root, so `vine sync` knows what to re-walk
├── handbook/
│ ├── _index.md # a branch, planted before the files under it
│ └── onboarding.md
└── policies/
├── _index.md
├── expense-policy.md
└── travel.mdEvery one of those files is a plain markdown document: YAML frontmatter (the machine interface) above a body (the human and model interface). Open the forest in Obsidian, VS Code or GitHub and it reads as notes, because that is what it is. And every write was a commit:
cd brain && git log --onelinea91f0c2 plant(policies/travel): Travel [source=ingest]
7d3be18 plant(policies/expense-policy): Expense policy [source=ingest]
2c40ab7 plant(policies/_index): policies [source=ingest]
4f1c2a9 init: forest 'My brain' (empty A.5 skeleton)The first read creates a third layer, and this one is disposable by design:
brain/_derived/
├── catalog.db # frontmatter, trails, degree + the FTS5 index
├── trails.db # pheromone heat, per node and per session
└── traces/
└── 8f2c1e4a9b07.jsonl # one line per primitive call (spec Part D)| Layer | Where | Versioned | If you delete it |
|---|---|---|---|
| Nodes | *.md anywhere in the forest | Yes, one commit per write | You lost knowledge. This is the product. |
| Payloads | *.db beside a dataset node, _assets/ | No. The node carries payload and payload_hash only | The passport survives and reports the drift. |
| Derived | _derived/ | No, gitignored | Nothing but time. vine reindex rebuilds the catalog; heat and traces restart empty. |
Binaries never enter the forest's git, and that is enforced at the commit layer rather than left to convention: the engine stages .md files and refuses everything else. Git delta-compresses text, not blobs, so a frequently rewritten spreadsheet would otherwise make the repository unusable within a month.
Troubleshooting
error: externally-managed-environment
Homebrew Python on macOS and the system Python on Debian and Ubuntu mark themselves managed by the OS package manager (PEP 668), so pip install -e . refuses to touch them. Create a virtualenv in the clone and install into that:
cd MonkeyLLM
python3 -m venv .venv
source .venv/bin/activate # macOS and Linux
.venv\Scripts\activate # Windows PowerShell or cmd
pip install -e .
vine --help # now resolves from the venvThe activation line is what puts vine on your PATH, so every later command in this page assumes an activated environment. A new terminal needs it again. If you prefer not to manage one yourself, pipx install -e . or uv pip install -e . both create the environment for you.
is not a forest (no _meta/schema.md)
Every verb except init and snapshot restore operates on an existing forest, and --forest defaults to the current directory. Without the guard it would be easy to run vine reindex from your project root and have it write _derived/ and .vine.lock there. A real forest always carries _meta/schema.md, written by init, and its absence means "not a forest", never "an empty one". Pass --forest, or run vine init first.
The mirror image, already a forest, is init refusing to overwrite an existing _index.md. That is also deliberate.
Files come back as unsupported
The report names every file no converter claimed. Two common reasons:
- The optional readers are not installed. The
.docx,.xlsxand.xlsconverters register only when their library imports, so without the extra those files are simply not claimed. Installpip install -e ".[ingest]"and runvine sync: they will be picked up as new. - It is a PDF. There is no built-in PDF converter. Name any CLI extractor you trust in the forest's Gardener config and it joins the pipeline like any other converter, with
{input}and{output}substituted for you.
# brain/_meta/gardener.yaml
converters:
".pdf": 'pdf2md "{input}" -o "{output}"'source contains the forest itself
The Gardener refuses a source directory that is the forest root, contains it, or sits inside it. A source above the forest would ingest the forest into itself, and every other forest sitting beside it, in one call. Keep the documents outside the forest folder. The companion refusal, this forest has no adopted source to sync, is vine sync declining to fall back to your working directory when no source was ever recorded.
E_LOCKED, or a stale .vine.lock
There is exactly one writing Vine per forest. If a process died holding the lock, delete .vine.lock at the forest root. If you actually want several readers, open them read-only: in Python Vine(root, writable=False), on the CLI vine serve --readonly.
Next steps
You now have a git-versioned forest that any MCP client can navigate. The two useful directions from here are understanding what the engine is doing on your behalf, and putting a governed host in front of it.
If you would rather read the source of everything on this page, the engine is in src/monkeyllm/ in the repository, and the normative specification is the highest numbered docs/monkeyllm-spec-v*.md beside it.