Skip to content
MonkeyLLMDocs

Connect

MCP server

MonkeyLLM speaks MCP natively, so connecting an agent is not an integration bolted on the side: it is the front door. A forest on your laptop over stdio, or a governed Station over HTTP, presents the same tools to the same harness. The difference between them is a key and a scope, never a change to the contract.

On this page(24)

Two servers, one contract

There are two MCP servers in the project, and they are deliberately contract-compatible. An agent that works against a forest on your own disk works against a Station-served forest with no change beyond the endpoint and a credential.

HTTPstdio
Your agentany MCP harness
Station /mcpkey, policy, scope
Enginethe primitives
Forestmarkdown + git
vine servestdio, no accounts
Every Station surface reaches a forest through ScopedVine and nothing else, so an unscoped handle is unreachable by construction. The local server has no policy layer because it has no identities to enforce one against.
Engine server against Station server
vine serveStation /mcp
Server namevinemonkeyllm-station
Transportsstdio, streamable HTTPstreamable HTTP
AuthenticationNone. Whoever runs the process holds the forestAn API key per request, resolved to a principal and a mask
The forest argumentOptional in single-forest mode, required with --rootRequired on every tool
Extra toolsclose_sessionanswer, ingest
AuditGit history onlyGit history plus a host access log per call

The rest of this page documents the Station surface, because that is what a shared deployment exposes. Where the local server differs, it is called out.

Transports

stdio

The engine speaks stdio by default: the client launches vine serve as a subprocess and talks over its pipes. No port, no network, no accounts. For a forest on your own machine this is the whole deployment.

bash
# One forest, stdio. The default, and the whole deployment.
vine serve --forest ./brain

# Read-only, for an agent you do not want writing yet.
vine serve --forest ./brain --readonly

# Registry mode: every subdirectory with an _index.md is servable, opened
# lazily on first touch. Tools then require forest=<id>.
vine serve --root /forests

In registry mode every subdirectory of --root that contains an _index.md is servable and opened lazily on first touch, auto-indexing itself if its catalog is empty. Tools then require forest=<id>, and forests() lists what is available.

Streamable HTTP

Over the network, both servers speak MCP streamable HTTP. The Station mounts its surface at /mcp and runs it in stateless mode with JSON responses: each HTTP request is handled in its own task, which is what lets the key that authenticated a request be the key the tool body reads. With sessions enabled, a tool call could be dispatched to a task created during an earlier request, so statelessness here is a correctness choice, not a performance one.

bash
# The engine over the network, no host, no accounts.
vine serve --forest ./brain --transport http --host 127.0.0.1 --port 8000

# The Station: identity, policy and audit around the same engine.
station serve --root /forests --registry /registry/station.db \
  --host 0.0.0.0 --port 8800 --writable

Mind the trailing slash

The Station's MCP endpoint is https://station.example.com/mcp/. The inner app is served at its own root and mounted under /mcp by the Station, so register the URL exactly as written, with the trailing slash.

Verify the server

Do this before touching a client config. Almost every "my agent shows no tools" report is a server that never started, a path that did not resolve, or a key with no grants, and all three are one curl away from being ruled out. Run the HTTP transport for the check even if you intend to deploy over stdio: a stdio server has no endpoint to probe, because the client is supposed to spawn it.

# 1. Start it in a terminal you can watch.
vine serve --forest /absolute/path/to/brain --transport http --port 8000

# 2. The engine's transport is session-based and answers as SSE, so the
#    handshake comes first. The session id comes back as a header.
curl -isX POST http://127.0.0.1:8000/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"curl","version":"0"}}}' | grep -i mcp-session-id

# 3. Ask it what it can do, quoting that session id.
curl -sX POST http://127.0.0.1:8000/mcp \
  -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "mcp-session-id: <the id from step 2>" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

A tool list comes back with look, locate, sniff, move, pick, scan, harvest and the rest, filtered by what the key may do. That is the whole proof: the process is up, the forest opened, and the credential resolves. Only then wire the client.

jsontools/list result, abridged
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      { "name": "forests", "description": "…" },
      { "name": "locate",  "description": "…" },
      { "name": "look",    "description": "…" },
      { "name": "move",    "description": "…" },
      { "name": "pick",    "description": "…" },
      { "name": "scan",    "description": "…" },
      { "name": "sniff",   "description": "…" },
      { "name": "harvest", "description": "…" }
    ]
  }
}

When no tools show up

  • vine: command not found, from the client only. A GUI client launched from the dock does not inherit your shell PATH, so a vine installed in a virtualenv is invisible to it even though your terminal finds it. Put the absolute interpreter path in the config: /path/to/.venv/bin/vine.
  • A relative forest path in the config. --forest ./brain resolves against the client's working directory, which is not yours and is rarely documented. Always write an absolute path in mcp.json, which is why every stdio example on this page does.
  • The server exits immediately with is not a forest (no _meta/schema.md). The path exists but was never vine init-ed, or it points one level above or below the real root.
  • E_LOCKED, or a server that starts and answers nothing. A forest takes one writer. If another process (a second client, a leftover vine serve) holds it, start this one --readonly or stop the other.
  • Connected, but the tool list is short or empty. That is authorisation, not transport: the key's grants decide which tools are advertised. Check them with GET /v1/me on the Station.
  • Nothing in the terminal. A stdio server writes its diagnostics to the client, not to you. Every MCP client keeps its own log: Claude Code under ~/.claude/logs/, most editors under their own extension log pane. Read that before rebuilding the config.

Connect an agent

Pair a key

Your agent needs a credential that is yours, not one an administrator has to mint. Pairing is that door: it is unauthenticated like login, takes your own username and password, and answers with an API key.

bash
curl -sX POST https://station.example.com/v1/auth/pair \
  -H 'content-type: application/json' \
  -d '{"username": "jimmy", "password": "...", "label": "claude-code"}'
json200 OK
{
  "api_key": "mk_D5vN8hZq2WcJ1yLpXeT7bR0mKsA4gFuI6oQnV3rYtBd",
  "principal": "jimmy",
  "caps": ["ingest", "read"],
  "expires_at": "2026-11-13T09:41:12+00:00"
}

What makes a paired key safe to hand to a machine is that it can only narrow, never add:

  • The mask is a ceiling. A paired key carries {read, ingest} by default, and that set is also the maximum. Asking for write, tend, query or admin is refused: those stay what an administrator mints deliberately.
  • Grants intersect the mask at the moment of use. The key's effective authority is your own grants filtered through the mask, computed live. A grant revoked after pairing is gone from the key immediately, and a paired key held by the owner is still refused every admin route.
  • It always expires. 90 days by default, 365 at most. There is no unlimited, and the key is shown once: only its digest is stored.

The key lives where every key lives. An administrator can revoke it from the Access console at any time, and the revocation takes effect on the next call.

Claude Code

With the paired key in hand, the whole connection is one registration.

claude mcp add --transport http monkeyllm \
  https://station.example.com/mcp/ \
  --header "Authorization: Bearer mk_D5vN8hZq2WcJ1yLpXeT7bR0mKsA4gFuI6oQnV3rYtBd"

From then on the forest's tools are in every session. Have the agent call forests() first: a scoped key has no access to the master index, so that call is how it learns which forests it may use and which roots to start from.

Any other MCP client

Nothing above is particular to Claude Code beyond the command. Any MCP-capable runtime connects with the same endpoint and the same key. Register it wherever your runtime configures MCP servers:

{
  "mcpServers": {
    "monkeyllm": {
      "type": "http",
      "url": "https://station.example.com/mcp/",
      "headers": {
        "Authorization": "Bearer mk_D5vN8hZq2WcJ1yLpXeT7bR0mKsA4gFuI6oQnV3rYtBd"
      }
    }
  }
}

The Station also accepts the key in an X-Api-Key header if your client finds that easier than a bearer token. Both are resolved at the same gate.

Allowed hosts

The MCP mount only answers requests whose host is on an allowlist. The default covers a local install (localhost, 127.0.0.1, both with and without port 8800). If you serve through a domain, name it.

bash
# docker-compose.yml, or wherever the Station's environment lives
MONKEYLLM_STATION_ALLOWED_HOSTS=station.example.com,localhost:8800

# Skip the check entirely. Every request still needs a key.
MONKEYLLM_STATION_ALLOWED_HOSTS=*

DNS-rebinding protection defends servers that trust the browser's ambient credentials. Every request here carries an API key an attacker cannot supply, which is why the deployment's own host list is the right control rather than a hardcoded one. Setting * disables the check and changes nothing about authentication.

The tools

The tools are the engine primitives plus the composites, each behind the capability it needs. forests answers to any valid key; everything else is gated. A capability the key does not hold is refused with E_FORBIDDEN naming the capability, never by hiding the tool: an agent that can see what it is missing can tell you.

The MCP tools
ToolNeedsWhat it does
forestsany keyLists the forests this key may use, with capabilities and starting roots
locatereadRanked entry points over curated metadata: where to drop in
lookreadCheap digest of one node: summary, edges, children, stats
movereadNeighbours of a node along typed edges
pickreadReads the body, or one section of it
scanreadFilters a branch's nodes by metadata
sniffreadLiteral search inside bodies: the facts summaries do not carry
harvestreadOne-shot retrieval: ranked evidence with exact snippets, no hops
answerreadA grounded answer written by the model bound to the forest, with its evidence
viewreadThe image behind a media node, as image content your model reads directly
queryqueryRead-only SQL against a dataset node
plantwriteCreates a node
graftwriteEdits a node
tendtendSingle-statement dataset write
ingestingestPuts documents into the forest through the Gardener

A reasonable mental model: answer and harvest are the one-shots, the locate / look / move / pick / scan / sniff family is navigation, query and tend are the dataset pair, and plant, graft and ingest are how the forest grows.

forests

Takes no arguments. Call it first, every session. It returns only the forests this key holds a grant on, and for each one the roots to start from, because a scoped key has no access to the master _index. When the key carries a capability mask, the caps reported are the masked ones: what the agent is told it may do is what the key can actually do.

jsonforests() on a scoped key
{
  "forests": [
    {
      "id": "handbook",
      "caps": ["ingest", "read"],
      "roots": ["policies/_index"]
    }
  ]
}

Every tool below takes forest as its first argument on the Station surface. Full response shapes are in the REST reference: the tool returns exactly what the REST route returns, because it is the same dispatch.

locate(forest, query, k, scope, type_filter)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id from forests().
querystringrequirednoneFree text, matched against curated metadata rather than bodies.
kintegeroptional5How many entry points to return.
scopestringoptional"all"all, branches or bananas.
type_filterstringoptionalnoneKeep only one node type.

look(forest, id, fields)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
idstringrequirednoneThe node to digest.
fieldsstring[]optionalnoneReturn only these keys. Worth using on dataset nodes, where the query manual and sample rows each open the payload.

move(forest, id, rel, direction)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
idstringrequirednoneThe node to move from.
relstringoptionalnoneOne relation, or the special value "children" to list a branch's physical children.
directionstringoptional"out"out, in or both.

pick(forest, id, section)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
idstringrequirednoneThe node to read.
sectionstringoptionalnoneA header from the node's outline. Bodies over 4000 tokens come back as the outline plus a hint to ask for one section.

scan(forest, parent_id, filter, recursive, limit)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
parent_idstringrequirednoneThe branch to scan.
filterobjectoptionalnonePassport columns plus tags_any, updated_after, updated_before, created_after, min_confidence.
recursivebooleanoptionalfalseWalk the whole subtree.
limitintegeroptional50Clamped to the range 1 to 50.

sniff(forest, terms, scope, k)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
termsstring[]requirednoneOne to 8 literal terms, at least 2 characters each. Exact codes, names and numbers. No regular expressions.
scopestringoptionalnoneA branch id narrows to that subtree; a leaf node id greps inside that node alone.
kintegeroptional5Clamped to a maximum of 20.

harvest and answer

These are the two one-shots, and the choice between them is about who does the reasoning.

  • harvest runs no model at all. It sweeps locate and sniff, fuses the rankings, and hands back the material itself with exact snippets and trails. Your agent reasons over it. Use it when the agent will do the thinking.
  • answer runs the same sweep and then lets the model bound to the forest read it, returning a grounded answer with its evidence. Use it when the forest's answer is the answer, and you would rather not spend the agent's own context on the material.

harvest(forest, query, terms, k)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
querystringrequirednoneThe free-text question.
termsstring[]optionalnoneLiteral terms for the sniff half. Derived from the query when omitted.
kintegeroptional3Clamped to at least 1 and at most the deployment cap, 5 by default.

answer(forest, question, k, cache, reply_tokens)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
questionstringrequirednoneThe question to answer.
kintegeroptional3How many nodes the retrieval half brings back.
cachebooleanoptionaltrueA repeat may be served from the forest's answer store, labelled cached: true. Pass false to buy a fresh run, which replaces the stored one.
reply_tokensintegeroptionalnoneBound the reply for this call, clamped to the range 64 to 4000. Absent, the forest binding decides.

answer needs a bound model

answer is a composite, not a primitive: it needs a model bound to this forest's answer role. Without one it returns E_SCHEMA saying so. The model only ever sees material the key could already read, so binding a model cannot become a way around the policy. The local vine serve has no answer tool at all, because it has no bindings.

Datasets

A spreadsheet or a CSV that entered the forest is a real SQLite table, not chunked prose. The path an agent takes is look then query: the digest of a dataset node carries a query_manual with the tables and columns, sample rows, and the notes a person wrote about how to read the data.

query(forest, id, sql)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
idstringrequirednoneA dataset node with a sqlite payload.
sqlstringrequirednoneA single SELECT or WITH statement. LIMIT 200 is injected when absent, and execution is capped at 2 seconds.

A mistyped table or column comes back as E_QUERY_INVALID with the real names in the hint, so a generated query can correct itself without spending another call. When the grant pins the tables for a dataset, naming any other table is refused.

view

A media node's body is a machine-written description of an image. view hands your model the pixels themselves. It returns a JSON header beside an MCP image content block, so a multimodal client reads the image into its own context:

json
[
  {
    "id": "media/onboarding-screenshot",
    "media_type": "image/png",
    "size": 184223,
    "payload_hash": "7c1e0b9a4d2f8365b1e0c9a4d2f8365b1e0c9a4d"
  },
  <image content block>
]

Images only, local payloads only, bounded at 6 MiB. A node with no payload answers the same E_NOT_FOUND envelope a missing node does. A remote payload is refused rather than fetched inside a read, and anything that is not an image stays with the surface that already serves it: query for datasets, and the REST payload route for raw bytes of any kind.

Writing back

plant and graft are atomic git commits inside the forest, stamped with the acting principal. They need the write capability, which a paired key does not carry by default, so an agent that should write needs a key an administrator minted.

plant(forest, node) and graft(forest, id, patch)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
nodeobjectoptionalnoneplant only. At least id, type, parent, title and summary. Pass schema to birth a dataset with its SQLite payload and query manual.
idstringoptionalnonegraft only. The node to edit.
patchobjectoptionalnonegraft only: set_frontmatter, add_links, remove_links, append_section, replace_section, replace_body.

tend is the dataset write: one INSERT, UPDATE or DELETE per call, WHERE mandatory on the last two, and no DDL ever. The binary never enters git; what is committed is the node's markdown carrying the new payload hash.

There is no route by which an agent deletes a node

Nodes are created and edited, never removed over any surface. What an agent writes is a commit in the forest's history, and a mistake is corrected the way any mistake in git is corrected.

ingest

The write a paired key actually carries. It puts documents through the Gardener, the same converters, curation and commits an operator gets, so an agent ingests exactly as a person does.

ingest(forest, mode, files, path, dest, wait)

NameTypeRequiredDefaultDescription
foreststringrequirednoneForest id.
modestringoptional"upload"upload sends the documents themselves. adopt and sync mirror a directory the Station host can read and additionally need admin.
filesobject[]optionalnoneFor upload: entries of {"name": "notes.md", "text": "..."} or {"name": "report.docx", "b64": "..."}.
pathstringoptionalnoneThe host directory, for adopt and sync.
deststringoptionalnoneThe existing branch everything lands under. Required for a scoped key.
waitbooleanoptionaltrueWaits for the batch and returns the finished job. Pass false to get the running job id back immediately.

A batch runs as a job. Unlike the REST route, this tool waits by default, because an agent poll loop would be context spent on plumbing. One batch per forest at a time: a second one while the first runs answers E_LOCKED naming the job.

How a key narrows the forest

Scoping only ever narrows content. It never changes the shape of a response, and it never produces a distinguishable refusal. That is the property that lets one agent codebase work against a full forest and a tightly scoped one without branching.

Tool calllocate(forest, query)
Resolve the keyprincipal + capability mask
Grants ∩ maskcomputed at the moment of use
Over-fetchask the engine for headroom
Filter by scopeallow, deny, trails scrubbed
Cut to kcounts recomputed from survivors
Filtering happens before the visible cut, so a scoped key still receives a full k where there is material to fill it. Losing recall is possible; learning that something was hidden is not.

Four consequences an agent author should design around:

  • No existence oracle. A node outside the scope answers E_NOT_FOUND with node not found: {id}, byte for byte identical to a node that never existed. The same applies to a forest: no grant and no such forest both answer unknown forest.
  • No truncation oracle. Derived counts are recomputed from what survived. sniff reports scanned_nodes as the nodes the key can see, not the bodies the engine opened; a branch's coverage and a node's stats.degree count only visible children and edges.
  • Trails and edges are filtered too. The trail of ancestor ids on a result is scrubbed of nodes the key may not see, and a graph edge needs both ends visible, because one visible end discloses the other.
  • Writes refuse differently, on purpose. A plant outside the grant answers E_FORBIDDEN, not not-found: the caller supplied the id, so saying so discloses nothing.

A full session

Here is what the loop actually looks like from the agent's side. A paired key with {read, ingest}, scoped to policies/ in a forest called handbook. The user asked: do I need a receipt for a 30 euro lunch?

  1. 1

    Find out what this key can reach

    The agent has no master index, so it starts by asking. The reply names the forest and the root to begin at.

    pythoncall
    forests()
    jsonresult
    {
      "forests": [
        {
          "id": "handbook",
          "caps": ["ingest", "read"],
          "roots": ["policies/_index"]
        }
      ]
    }
  2. 2

    Drop near the answer

    Rather than reading the branch, the agent searches curated metadata for entry points. Two candidates come back, ranked, each with the summary that will decide the next hop.

    pythoncall
    locate(forest="handbook", query="expense receipts over 25 euros", k=3)
    jsonresult
    {
      "results": [
        {
          "id": "policies/expenses",
          "kind": "banana",
          "type": "note",
          "title": "Expense policy",
          "summary": "What the company reimburses, the receipt rule and the limits per category.",
          "trail": ["policies/_index"],
          "score": 1.2841,
          "heat": 0.34
        },
        {
          "id": "policies/travel",
          "kind": "banana",
          "type": "note",
          "title": "Travel booking",
          "summary": "How to book flights and hotels, and which fares need approval.",
          "trail": ["policies/_index"],
          "score": 0.7712,
          "heat": 0.05
        }
      ],
      "truncated": false
    }

    Note the trail: it begins at policies/_index, not at _index. The master index is outside this key's scope, so it was scrubbed. Nothing in the response says so.

  3. 3

    Read the digest before paying for the body

    The first result looks right, but its body is 812 tokens and the answer is probably in one section. look costs at most 500 tokens and names the sections.

    pythoncall
    look(forest="handbook", id="policies/expenses")
    jsonresult
    {
      "id": "policies/expenses",
      "type": "note",
      "title": "Expense policy",
      "summary": "What the company reimburses, the receipt rule and the limits per category.",
      "tags": ["finance", "policy"],
      "confidence": 0.9,
      "updated": "2026-07-30",
      "edges_out": [
        {
          "rel": "related-to",
          "target": "policies/travel",
          "target_summary": "How to book flights and hotels, and which fares need approval."
        }
      ],
      "edges_in": [],
      "outline": ["Limits", "Receipts", "Approvals"],
      "stats": { "body_tokens": 812, "degree": 1, "heat": 0.34 }
    }

    outline says there is a Receipts section. The agent now knows exactly what to ask for, and it has not spent the body yet.

  4. 4

    Take only the section that answers the question

    pythoncall
    pick(forest="handbook", id="policies/expenses", section="Receipts")
    jsonresult
    {
      "id": "policies/expenses",
      "title": "Expense policy",
      "section": "Receipts",
      "body": "## Receipts\n\nAny single item over 25 EUR needs a receipt attached to the claim. Below that, the line item alone is enough. Receipts are kept for seven years, so photograph them the day you spend.",
      "body_tokens": 148,
      "truncated": false
    }

    148 tokens instead of 812, and the fact is in them. Four calls, well under a thousand tokens of context, and the agent can cite the node it stands on.

  5. 5

    Answer, with the citation

    textwhat the user sees
    Yes: anything over 25 EUR needs a receipt attached to the claim, and
    below that the line item alone is enough. Receipts are kept for seven
    years, so photograph them the same day.
    
    Source: policies/expenses (Receipts).

    The hunt also deposited pheromone on the trail it used, so policies/expenses ranks a little higher for the next question that goes this way.

  6. 6

    Keep what is worth keeping

    The user then mentions the threshold changes in January. That is durable, so the agent offers to save it, using the write its key actually carries.

    pythoncall
    ingest(
      forest="handbook",
      mode="upload",
      dest="policies",
      files=[{
        "name": "receipt-threshold-2027.md",
        "text": "# Receipt threshold 2027\n\nFrom January the threshold rises to 40 EUR."
      }]
    )
    jsonresult
    {
      "job": {
        "id": "ing-4b7f21ac",
        "forest": "handbook",
        "mode": "upload",
        "state": "done",
        "done": 1,
        "total": 1,
        "current": null,
        "stage": null,
        "errors": 0,
        "started": "2026-08-15T09:41:12Z",
        "finished": "2026-08-15T09:41:19Z",
        "report": {
          "planted": ["policies/receipt-threshold-2027"],
          "branches": ["policies/_index"],
          "updated": [],
          "unchanged": [],
          "stale": [],
          "unsupported": [],
          "errors": [],
          "mode": "upload",
          "staged": ["receipt-threshold-2027.md"],
          "commit": "b2c9e7a1d0f83546c9b2e7a1d0f83546c9b2e7a1",
          "curated": true,
          "bound": true
        }
      }
    }

    It went through the Gardener like any other document: converted, curated into a passport, committed. The next session, and the next person, will find it through locate.

When to skip the loop

The same question through answer(forest, question) is one call and returns the grounded text with its evidence, at the cost of a provider round trip on the Station. Through harvest(forest, query) it is one call returning the material for the agent to read itself. The four-hop loop above is what you want when the agent is following a thread that a single sweep would not resolve.

The Skills console

A connected agent knows the tools exist. It does not yet have the habit of using them, and a tool an agent never reaches for is indistinguishable from a tool that is not installed. The Skills console in the Studio closes that gap: it generates a small instruction file that teaches an agent runtime to treat the open forest as its memory.

The file is generated in your browser, for that exact deployment. Every snippet on the page already carries the Station's real address and the open forest's name, so there is nothing to substitute by hand and no way for the instructions to drift from the deployment they describe. The Station gains no endpoint for it. It is available to anyone whose key can read the forest, never admin-gated, because pairing made the credential self-service and learning to connect must be too.

For Claude Code the file installs at:

text
~/.claude/skills/monkeyllm-memory/SKILL.md

Under the title This forest is your memory, it teaches three things:

  • Recall before you answer. For any question the forest could answer, recall first and reason after: answer when the forest's answer is the answer, harvest when the agent will reason over the material, locate then look then pick to navigate, sniff for literal text inside bodies, and query for datasets after a look, because the dataset's notes say what the columns mean. Cite node ids for anything asserted from the forest.
  • Save what is worth keeping. When the user states something durable, a decision, a fact, a preference, a correction, offer to keep it, using the write the key actually allows. It teaches ingest for a paired key and plant / graft only for keys that carry write.
  • Respect the contract. Never work around a refusal: say what was refused and which capability it needs. Every read is budgeted, and truncated: true means ask narrower, not retry harder.

The file's body is English regardless of the console language, on purpose: it addresses the model, not you. Hand the same file to any other runtime, adapted to however it loads system prompts or skills.

Errors and budgets

Tool failures come back as the same envelope every other surface uses, as the tool result rather than as a protocol error, so an agent can read the hint and correct itself.

{
  "error": {
    "code": "E_FORBIDDEN",
    "message": "missing or invalid API key",
    "hint": "Send Authorization: Bearer <key>."
  }
}

The third one is worth reading twice. It is not a permission error. It is the exact envelope a genuinely missing node produces, and that is deliberate: an error that said forbidden would itself disclose that something is there.

Every read answers within a declared token budget, and a cut result always says truncated: true. There is no silent cut anywhere.

Token budget per tool
ToolBudget
look500 tokens
move600 tokens
locate, scan, sniff800 tokens each
query2000 tokens
pick, harvest4000 tokens each

These budgets are why a forest stays navigable by a small local model, and why the skill file teaches ask narrower, not retry harder.

Next steps

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