Skip to content
MonkeyLLMDocs

Get started

Quickstart

Install the engine, create a forest, feed it a folder of documents, run a query against it, and hand it to an agent over MCP. No embedding model, no GPU, no account, no host.

On this page(16)

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 init creates 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

PDFs 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. 1

    Create an empty forest

    vine init turns 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. --title is required and becomes the master index heading; --summary is optional and gets a sensible default.

    bash
    vine init --forest ./brain --title "My brain"
    textinit output
    forest created at /home/you/brain (commit 4f1c2a9e)
    next: vine validate / vine serve, or plant() your first nodes
  2. 2

    Feed it a folder of documents

    vine adopt mirrors 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.

    bash
    vine adopt ./my-documents --forest ./brain

    The 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 output
    planted (3):
      policies/expense-policy
      policies/travel
      handbook/onboarding
    branches (2):
      policies/_index
      handbook/_index
    unsupported (1):
      archive/scan-2019.pdf

    Adding --curate sends 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. 3

    Check the result

    vine validate lints every node against the schema: required frontmatter, summary length, resolvable wikilinks, payload hash drift. Add --strict to make warnings fail too, which is what you want in CI.

    bash
    vine validate --forest ./brain
    textvalidate output
    0 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

pythonlocate.py
from monkeyllm import Vine

vine = Vine("./brain", writable=False)
print(vine.locate("expense policy", k=2))
vine.close()

Response

json
{
  "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

pythonharvest.py
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

json
{
  "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.

bash
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.

bash
vine serve --forest ./brain --readonly

One server can also hold many forests. --root is registry mode, and it is mutually exclusive with --forest.

bash
# Every subdirectory holding an _index.md becomes a servable forest.
# Tools then require forest=<id>, and forests() lists the ids.
vine serve --root ./forests

Over 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.

bash
vine serve --forest ./brain --transport http --host 127.0.0.1 --port 8000

# The MCP endpoint is then http://127.0.0.1:8000/mcp

This 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"]
    }
  }
}

The 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 needs forest=<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: true means 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:

text
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 legal

After 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:

text
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.md

Every 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:

bash
cd brain && git log --oneline
textgit log output
a91f0c2 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:

text
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)
What each storage layer holds
LayerWhereVersionedIf you delete it
Nodes*.md anywhere in the forestYes, one commit per writeYou lost knowledge. This is the product.
Payloads*.db beside a dataset node, _assets/No. The node carries payload and payload_hash onlyThe passport survives and reports the drift.
Derived_derived/No, gitignoredNothing 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:

bash
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 venv

The 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, .xlsx and .xls converters register only when their library imports, so without the extra those files are simply not claimed. Install pip install -e ".[ingest]" and run vine 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.
yaml_meta/gardener.yaml
# 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.

Documents MonkeyLLM v0.1.0. Last reviewed against the engine source on .