Start with Mavambo
Mavambo is a desktop application. You install it, open a folder, and ask for what you want in plain English — it reads your project, works out what to change, and shows you exactly what it plans to do before it does anything. No terminal required.
Using the app
Install it, open a folder, and get your first real result. Everything happens in the window — nothing to type into a terminal.
First result in 5 minutes → For developersBuilding a tool
Write a tool the agent can call, check it locally, publish it so other people can install it.
Working tool in 20 minutes →Using Mavambo
Six pages, all of them inside the app. Each one ends with something visible on your screen.
Install and open a project
Download the installer and run it, the same as any other Windows application. Mavambo brings everything it needs with it — there is nothing to install beforehand and nothing else on your computer is changed.
When it opens for the first time it asks you to choose a folder to work in. Pick a project you know well. Mavambo reads the files inside that folder and nothing outside it.
The project folder you chose, and nothing above it. Mavambo does not index your home directory, your other repositories, or anything you have not opened.
You should now see the workspace, with your project name in the header and an empty conversation.
Your first task
There is a box at the bottom of the window. Type what you want in ordinary English — you are talking to it, not typing commands.
Start with a question
The best first task is one that only reads. You get an answer straight away and nothing on your computer changes:
What is this project, and what are the main parts of it?Mavambo opens the files it needs and answers in plain language. You will see it working as it goes — which files it looked at, and what it concluded. Nothing is hidden.
Then ask for a change
Now something that actually edits a file. Describe the result you want, not the steps to get there:
The heading on the home page is spelled "Welcom". Fix it.This time Mavambo shows you the change first — the old text and the new text, side by side — and waits. Nothing is written until you approve it. If the change is not what you meant, reject it and say what you actually wanted.
That is working as intended, not a failure. When a request could reasonably mean two things, Mavambo asks rather than guessing and getting it wrong. Answer in the same box and it carries on.
"Explain what this file does." · "Find every place the old company name still appears." · "This button is the wrong colour — make it match the others." · "Write a short README for this project."
You should now see an answer in the conversation, and — after approving the second request — a corrected file in your project.
Why it asked permission
Mavambo asks before it runs a command, before it writes a file, and before it runs any tool you have added. Every time, one approval per action. This is the single behaviour that most distinguishes it, so it is worth understanding rather than clicking past.
Nothing happens behind your back. If you did not approve it, it did not happen.
Adding something is not the same as running it
When you add a tool, Mavambo downloads it, reads it, checks it, and files it away. It does not run it. No setup script executes, nothing is fetched from anywhere else. That is what makes adding a tool a safe thing to do while you are still deciding whether you trust it.
If a tool needs extra software packages to work, you are asked about that separately — because unlike copying files, installing packages does run code. Two different questions, asked at two different moments, deliberately.
What the dialog tells you
Before a tool is added you see everything it has declared about itself:
Two extra lines appear when they apply, and they are the two worth pausing on. Network means the tool reaches the internet. Reads lists, by name, the settings from your computer it will be handed — and when that line is present, the reassurance above narrows to match it. No tool may ever ask for Mavambo's own API key; those names are refused outright.
The dialog tells you what a tool can reach. It cannot tell you what the tool will do with what it reaches.
A screen capture tool needs no internet and no settings from your computer, and is still pointed at whatever happens to be on your screen — your password manager, a private message, somebody else's data. So read the description and, if it matters, look at the source repository. The capability lines are the beginning of the question, not the answer.
You should now be able to read any of these dialogs and say what the tool can and cannot get to.
Sessions
A session is one continuous conversation about one project. They are listed down the side of the window, you can rename them, and opening one puts you back exactly where you left off.
Start a new session whenever you move to an unrelated task. It keeps each conversation focused, makes them easier to find later, and — as the next page explains — costs you less.
What carries over between sessions: your files and your project, obviously. What does not: the conversation itself, and everything Mavambo had worked out about what you were doing.
The list of available tools is fixed at the moment a session starts. If you add a tool while a conversation is open, that conversation will not see it — you need to start a new session.
This is deliberate. A set of tools that changed underneath a running conversation would cause far stranger problems than one that stays put. But it catches everybody the first time.
Answering from your phone
Mavambo can reach you on WhatsApp. When a turn stops on a proposal and you are not at the machine, the approval arrives in your own Message yourself chat, and replying 1 unblocks it. What the session says next comes back to the same chat, and what you type there goes to the session.
Linking a phone
Open Settings › WhatsApp and press Link a phone. A code appears in the panel. On your phone, open WhatsApp › Settings › Linked devices › Link a device and scan it. The panel says when it is linked.
This is a one-off. The link is remembered, so opening Mavambo tomorrow does not ask you to scan anything again. There is nothing extra to install and no terminal involved.
What you type
One rule: commands start with two dashes. Anything without them is just a message, and goes to Mavambo as an instruction. That is the whole of it — you can hold a normal conversation without ever tripping a command by accident.
| You send | What happens |
|---|---|
--help | The list below, in the chat |
--list | Your sessions, to switch between them |
--new | Start a session in a folder on the computer |
--status | What this session is doing right now |
--stop | Cancel the running turn |
| anything else | Sent to the session as an instruction |
When it is waiting on you
| You send | What happens |
|---|---|
1 | Approve the proposal |
2 | Reject it |
| anything else | Feedback on the proposal, rather than a new instruction |
| a number, while a list is on screen | Opens that row |
A list on screen outranks a waiting proposal, because 1 means both “the first row” and “approve”. Opening a session re-reads it and announces the proposal again, so nothing is lost by choosing first.
Starting one from scratch
Nothing needs to be open on the computer first. Send --new and the chat offers you somewhere to put it.
--newYou get a numbered list of places to start. A › at the end of a row means there are folders inside it, and a row marked (a project you've used) is somewhere you have worked before. Reply with a number to go in.
Three commands move you around from there: --up goes back out one level, --more shows the next nine, and --new word narrows a long list to the rows matching that word.
When you are standing in the folder you want, --here starts the session:
--hereTo work somewhere that does not exist yet, --create makes the folder and starts a session inside it in one step — there is no need to create it and then go in:
--create pricing-pageEither way the chat confirms with the session name and the full path it landed in, then says what it is doing. The phone is attached to that new session immediately, so the next thing you type becomes its first instruction.
| You send | What happens |
|---|---|
| a number | Goes into that folder |
--here | Starts a session in the folder you are in |
--create name | Makes that folder here, and starts a session in it |
--up | Back out one folder |
--more | The next nine |
--new word | Narrows a long list |
A drive or a top-level root is too broad to be a project, and --here refuses it rather than start a session that can see everything on the disk. Go into one of the folders inside it first — the chat says so at the time, rather than failing quietly.
Choosing which session
When the phone connects it offers your sessions as a numbered list, and you reply with a number to open one. Each row carries a badge for what that session is doing — working, or waiting on you — which is what makes the menu worth opening even when you already know which session you want. Send --list at any time to see it again.
Only sessions that have a folder are listed. A session that was never pointed at one cannot be worked on from a phone, and pointing it at a folder is something you do in the app.
If a different session stops and needs you, that arrives as its own message telling you to send --list to switch. It deliberately has nothing to answer with 1, so a reflexive reply cannot approve the wrong project's command.
The commands used to be bare words, which reads beautifully until one of those words is the whole of something you meant to say. Sending “stop” as a remark would throw away a running turn, whereas a remark read as a message merely gets answered — so the two ways to be wrong are not equal.
You do not have to remember this perfectly. Send a bare stop, status or sessions and the chat replies with the command you probably meant, rather than guessing. Only an exact whole-message match triggers that, so “stop the retry loop” was never in question.
A linked phone can send messages and answer approvals, which is to say it can make this computer edit files and run commands. Treat the link the way you would treat the machine itself.
Unlink from the same panel whenever you want to. That erases the stored session as well, so nothing is left behind on disk and the next link asks for a fresh scan.
Keys and cost
Mavambo needs an API key from an AI provider to think. You paste it into Settings once. It is kept on your own computer, and it is never given to a tool — no tool can ask for it.
Every request costs a small amount, billed by that provider rather than by Mavambo. The usage dashboard shows what each session has spent, and the small meter in the header is the same figure at a glance. Watch it for a day and you will quickly get a feel for which kinds of request are expensive.
A long conversation costs more with every message, because the whole conversation is sent again each time you reply. Starting a fresh session for a new task is almost always cheaper than carrying on in an old one — and it is the single easiest way to keep costs down.
Models and providers
Mavambo does not ship a model. It talks to one you pay for, using your own key. Two providers are supported in v1.0: DeepSeek and OpenAI.
There is no fixed list of models. When you open Settings › API Key, Mavambo asks the provider which models your key can reach and offers those. A model released after this page was written will simply appear in the menu — you do not need a new version of Mavambo to use it.
DeepSeek
Reaches https://api.deepseek.com. Any model your key can reach is offered; deepseek-chat is the usual starting point.
- The effort control applies to every DeepSeek model.
- Its ladder is
low,high,max— not the low/medium/high most APIs use. DeepSeek acceptsmediumandxhighbut maps them ontohigh, so offering them would put two entries in the menu that do the same thing. - Effort defaults to
low. DeepSeek's own default ishigh, but raising it multiplies what you spend, so Mavambo leaves that as your decision — one click in the composer. - DeepSeek publishes a balance endpoint, so the meter in the header can show what is left against what you topped up.
OpenAI
Reaches https://api.openai.com/v1. Any model your key can reach is offered; gpt-4o-mini is a cheap place to start.
- The effort control is sent only to the reasoning families that document it —
o1,o3,o4andgpt-5. On any other OpenAI model the parameter is rejected outright, so Mavambo omits it and the control has no effect. - Its ladder is
low,medium,high, defaulting tomedium. Some models also acceptminimal,noneorxhigh, but which ones varies — a menu entry that fails on the model you happen to be using is worse than a shorter menu. - OpenAI publishes no balance endpoint, so there is no top-up meter on this card. Per-session token counts and costs are still tracked in Usage.
The OpenAI card accepts a custom base URL, so the same key field can point at OpenRouter, a local server, or anything else speaking the OpenAI wire format. The DeepSeek card is fixed to DeepSeek's own endpoint.
This is deliberate rather than an oversight. Claude models use a different endpoint and a different message shape for cache markers, so supporting them means a second call path rather than another row in a table. Both providers above speak the same format, which is why they share one.
Adding skills and tools
Mavambo can be taught new things. There are two kinds, and the difference is simple:
- A skill is instructions — it teaches Mavambo a procedure, a house style, a checklist. It is just writing. Nothing runs.
- A tool is a small program that lets Mavambo do something it otherwise cannot: read a PDF, pull text out of an image, talk to a service.
Adding one
Open Settings, choose Skills or Tools, and paste in the address of the repository that holds it. For example:
https://github.com/JakatiCourtneyPanashe/mavambo_tools/tree/main/pdfYou will get the dialog from the previous page — read it, then confirm. Now start a new session, because the tool list is fixed when a session begins.
Ask for something the new tool covers, and Mavambo will offer to use it:
Pull the invoice totals out of the PDFs in this folder.A skill or tool you never happen to need costs you nothing at all. They are offered only when they are relevant, so there is no penalty for adding one that turns out not to fit.
Next: once you have used someone else's tool, the natural next thought is wanting one of your own. That is track two — the only part of Mavambo where you run a command yourself, and even then just one: the checker that confirms a tool is correct before you publish it.
Building for Mavambo
Write a tool other people can add to their own copy of the app. Everything here is checked by one command you run before you publish anything.
Everything you install, remove, enable or disable happens in Settings, with buttons. Building a tool is the one exception, and it needs only two commands the whole way through: check, which tells you your tool is correct, and sync, which installs a tool's dependencies. Nothing else on this page is a command.
Both run from the Mavambo backend folder — the one holding main.py. If building a tool is not what you came for, track one is the whole of the app.
Skill or tool?
Two extension types, and people reach for the wrong one constantly. The rule of thumb: if you are about to write code that only rearranges words, you wanted a skill.
Skill
- Instructions in Markdown. No executable.
- Loaded into the model's context when relevant.
- Teaches a procedure, a house style, a checklist.
- Installing it is low risk — it is text.
- Follows the Agent Skills specification, so it works in other harnesses too.
Tool
- An executable the agent calls with typed arguments.
- Runs as a separate process in the project folder.
- Does something the model cannot: read a device, call an API, run a computation.
- Approval-gated on every single call.
- Mavambo-specific.
"Follow our commit message convention" is a skill. "Capture the screen and return the image path" is a tool. "Review a PR against our checklist" is a skill that may call tools.
Quickstart: your first tool
You are going to build a word counter. It is deliberately trivial: the point is to see the whole loop — write, check, install, get called — before any of the reference material matters.
Make the folder
Create a folder anywhere you like while you are working on it — you will point Mavambo at it later. The folder name and the name inside the manifest must match exactly. This catches nearly everyone once, and you are going to trip over it on purpose in step 5.
mkdir word_count && cd word_countTwo files go in it, and that is the whole tool.
Write the manifest
Everything the agent and the user will ever know about your tool before it runs:
{
"name": "word_count",
"description": "Count words and lines in text files in the project. Use when the user asks how long a document is, or to compare the length of several files.",
"entry": ["python", "handler.py"],
"timeout_seconds": 30,
"parameters": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"description": "Project-relative file paths to count."
}
},
"required": ["paths"],
"additionalProperties": false
}
}
A specific name. Tool names are global on a user's machine, and a collision is refused outright rather than resolved. word_count is specific enough; count or read would eventually block somebody else's tool, or theirs would block yours. If your tool is a thin wrapper around something generic, prefix it with your name or your organisation's.
The plural. paths, not path. Every call costs the user an approval, so a tool asked about twenty files should read twenty files in one call.
Write the handler
Read one JSON object from stdin, print one JSON object to stdout, exit 0. That is the entire contract:
import json, sys
from pathlib import Path
request = json.load(sys.stdin)
folder = Path(request["cwd"])
try:
counts = []
for name in request["arguments"]["paths"]:
text = (folder / name).read_text(encoding="utf-8")
counts.append({
"path": name,
"words": len(text.split()),
"lines": text.count("\n") + 1,
})
print(json.dumps({"result": {"files": counts}}))
except Exception as error:
# A failed tool call is a normal outcome, not a crash. Say what
# went wrong so the model can choose a different next step.
print(json.dumps({"error": str(error)}))
Check it
This is the command the rest of these docs are built around. It runs both gates locally: it validates the manifest, then actually launches your handler with a probe envelope and judges what comes back.
python main.py --mav tools check .Run it after every edit. It is the only place a contract mistake is cheap to find — the alternative is a live call failing with an opaque exit code three sessions from now.
Break it on purpose
Rename your folder to wordcount, leaving the manifest alone, and run the check again. You will get:
name 'word_count' must match its directory name 'wordcount'Now delete the description line and check again. Then put "pattern": "^[a-z]+$" into the paths item and check once more.
Three failures in ninety seconds, each naming its own fix. Doing this now means the validator reads as a collaborator for the rest of your time here, rather than as a wall you hit on publish day. Put everything back when you are done.
Add it and use it
Push the folder to a repository and add it through Settings → Tools, exactly as one of your users would — it is worth seeing your own install dialog at least once. Then start a new session, because the tool list is fixed when a session begins.
Now ask for something your description covers:
How long is the README compared to CONTRIBUTING?Mavambo will offer the call, show you the arguments it intends to pass, and wait. Approve it.
You have a working tool. Everything below explains what you just did and how to make it good enough for strangers.
The stdio contract
Your handler is launched as a subprocess in the active project folder. It gets one JSON object on stdin:
{
"tool": "word_count",
"arguments": { "paths": ["README.md", "CONTRIBUTING.md"] },
"cwd": "C:\\Users\\you\\projects\\thing"
}
It must print exactly one JSON object to stdout and exit 0. That object carries either result or error:
// success — anything JSON-serializable
{ "result": { "files": […] } }
// failure — a sentence the model can act on
{ "error": "README.md was not found in this project" }
stdout is the channel
Anything else you print corrupts the response. No progress lines, no print("starting…"), no stray library banner. If you want diagnostics, write to stderr — it is captured, bounded, and shown when something goes wrong.
Whatever you put in result enters the model's context and is paid for on every call. Anything large — an image, a binary, a 40MB log — is written to disk, and you return the path plus a little metadata.
This is not a style preference. Base64-ing a 4K screenshot into result is a five-figure token bill for one call, and the model cannot see images anyway.
A tool that cannot do its job still exits 0 and returns {"error": …}. A non-zero exit is reported as a broken tool rather than a failed call, which is a worse message for everyone. Reserve it for genuinely unhandled crashes.
Declaring parameters
parameters is a JSON Schema object and is required, even when your tool takes no arguments. It must be "type": "object", and you should always set "additionalProperties": false.
The validator implements eleven keywords
And here is the part that surprises people: a constraint keyword outside that set is refused, not ignored.
Enforced
Checked at call time. Use freely.
Allowed, inert
Documentation. Enforces nothing, and does not pretend to.
Refused
Everything else, including these.
If pattern were quietly accepted, you would write a constraint, believe your input was bounded, and have no way to see from outside that nothing enforced it. A documentation keyword that does nothing is honest. A constraint keyword that does nothing is a lie. So Mavambo rejects the manifest and tells you which keyword it cannot keep a promise about.
Types
Enforced types are object, array, string, integer, and boolean.
"number" is not in the type table. Declaring it does not loosen the check — it removes the check from that field entirely. Use "integer", or take a string and parse it yourself.
Size limits
At most 40 properties counted through nested objects and array items, and at most 5 levels deep. Both are measured against the widest and deepest built-in tools, so they bite third-party excess and not ordinary work.
Depth is the one to respect. A deeply nested argument cannot be composed from a signature line, which forces the model into an extra round trip before every single call — you pay for that nesting forever.
Write the description for a router, not a reader
Your description is how the model decides whether to reach for your tool at all. Say what it does and when to use it. Cap is 500 characters; a description longer than that is documentation, and documentation belongs in your README where nobody pays for it on every call.
Dependencies and capabilities
dependencies
A list of at most 20 requirements, each a package name with optional extras and version specifiers. Nothing else is accepted:
"dependencies": ["mss>=10", "pillow"] // fine
// all refused:
"git+https://github.com/me/thing" // a host nobody was shown
"./vendor/thing" // a path the tool chose
"--index-url=https://my.mirror" // a flag, not a package
"requests; os_name=='nt'" // machine-dependent
Every exclusion is a way to install code from somewhere the approval prompt did not name. The whole argument for a second prompt is that the user sees the packages before code from PyPI runs; a requirement that can point anywhere makes that dialog a decoration.
All installed tools share one virtual environment, and conflicts are detected across every installed manifest. Your == pin is not a private decision — it can block someone else's install months from now.
Declare the loosest constraint that actually works. If you can do the job without a dependency at all, do that: dependencies are the single biggest reason a user abandons an install.
Dependencies are never installed silently. If a user declines, or copies your folder in by hand, the tool stays installed and does not load — and the reported reason names the fix:
python main.py --mav tools syncThat repair is command line only for now. The app lists the tool, but has no button that runs it.
network
A boolean, default false. Set it to true if your tool reaches the internet at any point, and be honest about indirect cases — a library that downloads a model on first use makes your tool a network tool, however offline it feels.
environment
An allowlist of up to 10 environment variable names your tool will be handed — each an ordinary variable name in capitals, digits and underscores. It receives those and nothing else. Names belonging to Mavambo itself are refused at load, so no tool can ask for the agent's API key.
timeout_seconds
Default 30. For a local tool the ceiling is 180, and anything above it is clamped down silently rather than refused. A tool that declares network is the exception — it keeps whatever timeout it sets, with no ceiling at all, because Mavambo cannot know how long a remote host will take to answer. Either way the value is shown in the install dialog, so an absurd one is visible at the moment trust is granted. If a local tool genuinely cannot finish inside 180 seconds, it needs to do less per call — or write its output to disk and return a path.
Writing a skill
A skill is one folder with a SKILL.md in it. The frontmatter takes six fields, of which two matter:
---
name: commit-style
description: Write commit messages in our house convention. Use when
committing, amending, or rewriting history in this repository.
---
# Commit style
Subject line in the imperative, under 60 characters, no trailing period…
Skill names are lowercase and hyphenated — not the underscored style tools use. That is not an inconsistency: skills follow the Agent Skills specification, so a skill that passes here also loads in other harnesses. Tool names become internal action types and match the built-in tools instead.
It is not a title. It is the only thing the model sees when deciding whether to load your skill, so it must say when to use this and, where it helps, when not to. Up to 1024 characters — spend them.
The body is Markdown, capped at 60,000 characters. It flows straight into the model's context when the skill activates, so length has a real price. Anything long, optional, or reference-shaped belongs in a separate file the skill links to.
Publishing
Put the tool folder at the root of a public repository, or in a subdirectory of one — a repository can hold as many tools as you like, each in its own folder. That is how the reference tools are laid out.
Your users add it the same way you did in the quickstart: they paste the folder's address into Settings → Tools and confirm the dialog.
https://github.com/JakatiCourtneyPanashe/mavambo_tools/tree/main/pdfThe install pins to the commit it resolved, and that commit is shown in the dialog. Users need git installed; nothing else.
Before you push
- Make the name specific.
ocr_read, notread. The namespace is flat and collisions are refused outright. - Run
tools checkfrom a clean clone, not from the folder you have been editing. It catches the file you forgot to commit. - Read your own install dialog. Would you say yes to it from a stranger? If the description does not justify the capabilities, fix the description or drop a capability.
- Loosen your pins. See the shared environment note above.
- Write the README for an evaluator, not a user. The first question is not "how do I call this" — it is "what is this going to do on my machine".
Installs pin to a commit and nothing tells a user that a newer one exists. Updating today means uninstalling the tool in Settings → Tools and adding it again. Treat every release as one a share of your users will never receive, and keep breaking changes rare.
Reference
Lookup material. Nobody reads this section forward — search it.
tool.json fields
| Field | Required | Type | Rules |
|---|---|---|---|
name |
Yes | string | Lowercase letters, digits, single underscores, starting with a letter. Must match the folder name. Max 64 characters. Cannot collide with a built-in tool. Make it specific enough not to collide with somebody else's. |
description |
Yes | string | Max 500 characters. Says what the tool does and when to reach for it. Without one the model cannot select the tool. |
entry |
Yes | array | Non-empty. First element must be python or python3. Second must be a script path inside the tool's own folder. |
parameters |
Yes | object | A JSON Schema with "type": "object". Required even for a tool taking no arguments. Max 40 properties, max depth 5. |
timeout_seconds |
No | integer | Default 30. Local tools cap at 180 (higher is clamped, not refused); a network tool has no ceiling. |
dependencies |
No | array | At most 20. Package names with optional extras and version specifiers. No URLs, paths, pip flags, or environment markers. |
network |
No | boolean | Default false. Shown in the install dialog when true. |
environment |
No | array | At most 10 variable names (capitals, digits, underscores) to pass through. Listed by name in the install dialog. Mavambo's own variables are refused. |
Any field not in this table is refused — an unrecognised key is more often a typo than an extension.
Schema keywords
| Keyword | Status | Applies to |
|---|---|---|
type | Enforced | object, array, string, integer, boolean only |
properties | Enforced | objects |
required | Enforced | objects |
additionalProperties | Enforced | objects — set it to false |
items | Enforced | arrays |
minItems | Enforced | arrays |
enum | Enforced | any |
minLength / maxLength | Enforced | strings |
minimum / maximum | Enforced | integers |
description / title | Inert | documentation only |
| anything else | Refused | manifest will not load |
Developer commands
Mavambo is a desktop application, so adding, removing, listing, enabling and disabling skills and tools all happen in Settings — there is nothing to type for any of that. Only two commands have no button, because both are for building a tool rather than using one. If you are not writing a tool, you never need either.
| Command | What it does |
|---|---|
python main.py --mav tools check <path> | Validate a tool folder and probe its handler. Run it after every edit, and once more from a clean clone before you publish. |
python main.py --mav tools sync | Install any declared dependencies that are missing, across every installed tool, in one approval. This is the repair a tool names when it is installed but will not load. |
Both run from the Mavambo backend folder — the one holding main.py.
Tools live under Settings → Tools and skills under Settings → Skills. Paste a repository address to add one; use its enable/disable switch to turn it on or off, and the uninstall button to remove it. No command needed for any of these.
Error index
Paste the message you got into search, or find it here. Every entry is the literal text Mavambo prints.
name 'x' must match its directory name 'y'
The name in your manifest and the folder holding it disagree.
Fix: rename one of them. The name becomes an internal action type and the folder is how it is found, so both have to agree before either can be trusted.
name 'x' is already a built-in tool; installed tools are refused on a collision rather than shadowing it
You picked a name Mavambo already uses.
Fix: pick a more specific name, or prefix it with your own. Shadowing is refused deliberately — precedence rules between a built-in and an installed tool produce the worst failure available, which is the wrong code running under a familiar name.
name 'x' is not valid: use lowercase letters, digits and single underscores, as the built-in tools do
Uppercase, hyphens, a leading or trailing underscore, or a doubled one.
Fix: use the style the built-in tools use — ocr_read, word_count. Note that skills follow the opposite convention, lowercase and hyphens, because they implement a published specification.
'description' is missing; without one the model has no way to know when to reach for this tool
No description, or an empty one.
Fix: write one sentence on what it does and one on when to use it. This is a routing decision, not a label.
entry runs 'node'; step 4 supports python and python3 only
The first element of entry is not a supported interpreter.
Fix: use ["python", "handler.py"]. The runtime itself is language-agnostic — an argv array and JSON over stdio would launch anything — but only Python is proven by the test fixtures today.
'entry' must be a non-empty array, e.g. ["python", "handler.py"]
entry is missing, a string, or empty.
Fix: an array of strings — interpreter first, then the script.
entry script 'x' escapes the tool's own folder
The script path resolves outside the tool directory — usually a ../ or an absolute path.
Fix: keep every file the tool runs inside its own folder. A tool that can point its entry anywhere is a tool the install dialog cannot describe truthfully.
'parameters' is missing; declare an object schema even if it takes none
No parameters key.
Fix: {"type": "object", "properties": {}, "required": [], "additionalProperties": false}.
tool.json is not valid JSON
A syntax error — most often a trailing comma, or a comment. JSON has no comments.
Fix: the message names the line and column.
git was not found on PATH. Installing needs git.
Installing from a repository clones it, and that needs git.
Fix: install git and restart Mavambo so it picks up the new PATH. Everything else in Mavambo works without it.
[tools] 1 installed tool could not be loaded and the model has not been told about it
Printed at session start. A tool is present on disk but did not pass validation, so the model was never told it exists.
Fix: open Settings → Tools to see the specific reason next to that tool. This line exists because a broken tool is otherwise invisible — the session simply proceeds slightly less capable than it should be.
Reference tools
Two published, working tools to read, clone, and copy from. Both live in one repository, each in its own folder — which is also the layout to copy if you plan to publish more than one thing.
They are a deliberate pair. Mavambo reads text, not pictures, so anything that arrives as an image or a page layout has to be turned into text before the agent can reason about it at all. Between them these two cover the cheap case and the expensive one.
Reads text and tables out of PDF files in the project and returns them. The one to read first — it is small enough to hold in your head, takes a collection of paths rather than one, and has a light dependency footprint.
ocr_read
Pulls text out of images — screenshots, scans, photographs of documents. The honest version of a tool with real costs: heavier dependencies, a longer timeout, and a capability its author would rather not have needed.
In pdf: how the schema takes paths rather than path, and how the handler reports a missing file as an error the model can act on rather than crashing.
In ocr_read: why it declares network even though reading an image sounds like an offline job, and how the timeout is justified. Those are the two declarations users hesitate over, and seeing them argued for honestly is more useful than seeing them avoided.
Known limits
Written down rather than discovered. All three are things we intend to change.
- The tool namespace is flat. Names are global on a user's machine and collisions are refused outright. Until that changes, choosing a specific name is the whole defence — which is why these docs push it so hard.
- The dependency environment is shared. One virtual environment across every installed tool. Conflicts are detected and reported honestly, but a strict pin in one tool can block an install months later.
- There is no update command. Installs pin to a commit and nothing surfaces a newer one. Remove and reinstall is the current answer.
Installing executes nothing, and dependency installation is asked separately. Every convenience above will be built around that property rather than through it.