mongo-axi
v0.1.4
Published
Read-only MongoDB CLI for agents - schemas inferred from samples, capped find/aggregate/count/distinct/explain in token-efficient TOON. No JavaScript is evaluated; write stages are refused and every command is checked against a read allowlist before it is
Maintainers
Readme
mongo-axi
Read-only MongoDB CLI for agents, following the AXI principles. It lists databases and collections, infers schemas from samples, and runs capped find, aggregate, count, distinct and explain. Output is token-efficient TOON.
It cannot change data. For writing to a local dev database there is a separate package, mongo-rw-axi.
npm install -g mongo-axi
printf %s "$DB_PASSWORD" | mongo-axi auth login --profile dev --host 127.0.0.1 --user app --auth-source admin -d shop --password-stdin
mongo-axi # live overview: server, then the default database's collections
mongo-axi collections --sort size
mongo-axi describe orders customers
mongo-axi find orders '{status: "paid", created_at: {$gte: ISODate("2026-10-01")}}' --sort '{created_at: -1}'
mongo-axi aggregate orders '[{$group: {_id: "$status", n: {$sum: 1}}}]'
mongo-axi 'db.orders.find({status: "paid"}).limit(5)' # mongosh syntax, parsed as dataOutput is strict TOON apart from raw text (explain plans, validators, view pipelines) between --- fences. For scripts, --json anywhere on the line prints the same payload as JSON, with raw text as a string field (plan, validator, pipeline) and typed values in their mongosh form ("ObjectId('…')"); errors keep their exit code:
mongo-axi find orders '{status: "paid"}' --project '{total: 1}' --json | jq -r '.result[]?.total'Safe to auto-approve
An agent with mongosh through Bash can run anything its account allows, including JavaScript, so each call needs a human to read it first. mongo-axi is read-only by construction: filters are parsed as data, write stages and server-side JavaScript are refused, every command is checked against a read allowlist just before it is sent, and every query carries a 30 s maxTimeMS. So Bash(mongo-axi:*) can be allowed without prompts.
The same commands sent by an agent through each tool, against a throwaway MongoDB 8.0 as the root user the official Docker image creates, which is what a local dev setup usually has. Every cell is checked by test/integration/safety.test.ts in CI. ran means the effect was on the server afterwards.
| Command from the agent | mongosh --eval | mongodb-mcp-server --readOnly 3.1.0 | mongo-axi |
| --- | --- | --- | --- |
| Reads an agent needs | | | |
| explain a find - does it use an index? | works | works | works |
| $group orders by status | works | works | works |
| Mistakes | | | |
| deleteMany | ran | blocked by the tool | refused; points to mongo-rw-axi (loopback only) |
| updateMany with an empty filter | ran | blocked by the tool | blocked by the tool |
| drop() a collection | ran | blocked by the tool | blocked by the tool |
| find() everything in a 10,000-document collection | 10,000 documents; ≈475k tokens | 10 documents; ≈1k tokens | 20 documents; ≈1k tokens |
| Ways around a read-only mode | | | |
| $out at the end of a pipeline | ran | blocked by the tool | blocked by the tool |
| $merge back into the collection it reads | ran | blocked by the tool | blocked by the tool |
| $where running JavaScript on the server (sleep) | ran | blocked by the tool | blocked by the tool |
| read admin.system.users (password hashes) | ran | ran | refused; credential collections are never read |
| a slow scan - is a server-side time limit sent? | no time limit | no time limit | stopped by maxTimeMS (30 s default) |
mongodb-mcp-server 3.1.0 in --readOnly mode stops the same writes and server-side JavaScript. The differences are in the last two rows: it returns system.users (password hashes) to the agent, and it sends no time limit unless you configure maxTimeMS.
How writes are blocked
MongoDB has no read-only session, so the account's roles are the only server-side check. The client carries the rest, in four independent layers:
- Data, not code. Filters, projections, sorts and pipelines are JSON, Extended JSON or mongosh literals (
ObjectId('…'),ISODate('…'), unquoted keys,/regex/i). A strict parser turns them into BSON values; nothing is evaluated. The mongosh shell form (db.orders.find(...).sort(...)) is parsed the same way, soforEach(function …), a second statement orthisis a syntax error.Codevalues and$codeare refused. - An input guard, before connecting. It walks every level of the parsed input and refuses, in any letter case:
$outand$merge(write stages);$where,$functionand$accumulator(server-side JavaScript);$changeStream(never returns) and internal$_…stages;- reads of
system.usersandsystem.keys, also through$lookup,$graphLookupand$unionWith.
- A wire allowlist. The driver emits
commandStartedsynchronously before a command reaches the socket, and a listener that throws stops it. Every command is checked there against a read allowlist (find,aggregate,getMore,count,distinct,explain,list*,*Stats,hello,ping,buildInfo,connectionStatus,killCursors,endSessions). Aggregate and explain bodies are checked again, andfind/aggregate/count/distinct, explained or not, must carrymaxTimeMS. This catches a bug in layers 1-2 or in the commands themselves.doctorproves it on every run by sending a non-read command and checking that it was stopped. - No write code in the package. Write commands live only in
mongo-rw-axi. The build fails if write-tool code shows up in themongo-axibundle.
Limitations
- It is not a role check. With write roles, a bug in all of the layers above could still write. For a hard guarantee, connect with a read-only user:
db.getSiblingDB("admin").createUser({user: "agent_ro", pwd: passwordPrompt(), roles: [{role: "read", db: "shop"}]})mongo-axi doctorlists the account's roles and warns when it can write or when the server has no access control. - Reads can still be expensive. Each query stops at 30 s (
--timeout), but a heavy$lookupor$groupuses server resources until then. explain --analyzeruns the query (read-only, time-capped) to get execution stats.- Keys named
password,pwdandcredentialsprint as<redacted>. Other sensitive fields print as stored. - Results are capped at 20 documents (max 1000,
--limit). A cut result reports the total up to 1000 (docs: 20 of 72 shown), so one rerun with--limit 72gets them all. Strings are cut at 200 characters, arrays at 20 items and nesting at depth 8;--fulllifts all three. describeandsearchinfer fields from a sample (100 and 20 documents), so a rare field can be missing.- MongoDB 4.0 to 8.x (driver 6.16, wire version 7 and up). The integration suite also runs on
mongo:3.7, the 4.0 development series.
Recipes
These are the flows in test/integration/scenarios.test.ts. The fixes need the local-only mongo-rw-axi.
Slow query: find the collection scan, add the index, check the plan.
mongo-axi explain orders '{customer_id: ObjectId("…"), created_at: {$gte: ISODate("2026-09-10")}}'
# -> COLLSCAN filter {...}; help: an index on customer_id would let it use IXSCAN
mongo-rw-axi index create orders '{customer_id: 1, created_at: -1}'
mongo-axi explain orders '<same filter>'
# -> IXSCAN customer_id_1_created_at_-1 (customer_id 1; created_at -1)A field with mixed types: a filter on one type misses the others.
mongo-axi describe orders # help: Mixed types (total int|string)
mongo-axi find orders '{total: {$type: "string"}}' --project '{total: 1}'
mongo-rw-axi update orders '{total: {$type: "string"}}' '[{$set: {total: {$toDouble: "$total"}}}]' --manyReferences to documents that do not exist.
mongo-axi aggregate reviews '[{$lookup: {from: "orders", localField: "order_id", foreignField: "_id", as: "order"}}, {$match: {order: {$size: 0}}}, {$project: {order: 0}}]'Before a unique index: find the duplicates.
mongo-axi aggregate signups '[{$group: {_id: {$toLower: "$email"}, n: {$sum: 1}, ids: {$push: "$_id"}}}, {$match: {n: {$gt: 1}}}]'
mongo-rw-axi delete signups '{_id: ObjectId("…")}'
mongo-rw-axi index create signups '{email: 1}' --uniqueAn index nobody uses.
mongo-axi indexes orders # ops per index since the last restart; help lists the unused onesConnections
- Profiles:
auth logincreates one. The password goes in the OS keychain and the rest goes in~/.config/mongo-axi/config.json.--uritakes any connection string without the password (mongodb+srv://,replicaSet=,tls=trueand so on). - Env override:
MONGO_AXI_URL=mongodb://user:password@host:port/database?authSource=adminoverrides the profiles. - Picking a profile: use
-p/--profile <name>(before or after the command), orMONGO_AXI_PROFILEfor a single command.-d/--databasepicks the database;database.collectionworks too.
Live overview
Run mongo-axi with no command to see the server version and topology. It then shows the default database's collections, or the largest databases when no database is set, and the next commands to try. It connects with short limits: 3 s to reach the server and 10 s in total. When it cannot connect it prints live: unavailable - <why> with a hint and still exits 0. -p and -d work without a command: mongo-axi -p staging -d shop.
$ mongo-axi
bin: ~/.npm-global/bin/mongo-axi
description: "READ-ONLY MongoDB CLI for agents - infer schemas and run capped read queries, with TOON output. Filters are parsed as data (never evaluated) and every command is checked against a read allowlist before it is sent (not a role check)."
mode: READ-ONLY - find; aggregate; count; distinct; explain. Write stages and server-side JavaScript are refused; results capped at 20 docs
connection: "[email protected]:27017/shop (profile dev)"
server: MongoDB 8.0.4 - standalone
database: shop - 4 collections; 1 views; 100 KB
collections[5]: customers,events,orders,paid_orders,reviews
help[2]:
- Run `mongo-axi collections` for counts and sizes; `mongo-axi describe <collection>` for fields and indexes
- "Run `mongo-axi find <collection> '{status: \"paid\"}'`, `mongo-axi aggregate <collection> '[...]'` or `mongo-axi 'db.<collection>.find({...})'`; `mongo-axi <command> --help` for flags"Agent integration
There are two ways to tell an agent about mongo-axi. You only need one.
| | Session hook | Skill |
|---|---|---|
| Install | mongo-axi setup hooks | npx skills add cmosgh/mongo-axi --skill mongo-axi |
| What the agent gets | a summary at every session start (about 250 tokens): the connection, the profiles, what the last session in this directory ran, and the commands to try | a short skill that loads when a task needs MongoDB and points at mongo-axi --help |
| Cost | every session | only when the task needs it |
| Agents | Claude Code, Codex, OpenCode | any agent that reads Agent Skills |
Session hook
mongo-axi setup hooks installs two hooks:
- Session start (Claude Code, Codex, OpenCode) runs
mongo-axi context, which prints the summary below. It reads only the config file. It never connects or reads the keychain, so it adds no delay. - Session end (Claude Code) runs
mongo-axi context end. It reads the session transcript and records whichmongo-axicommands ran: command names, counts and the-p/-dvalues, never filters or other arguments. The record goes in~/.config/mongo-axi/sessions.json, keyed by directory, and the next session in that directory sees it aslast_session.
$ mongo-axi context
bin: ~/.npm-global/bin/mongo-axi
description: "READ-ONLY MongoDB CLI for agents - ..."
mode: READ-ONLY - find; aggregate; count; distinct; explain. Write stages and server-side JavaScript are refused; results capped at 20 docs
profiles[1]{name,target,active}:
dev,"[email protected]:27017/shop",true
last_session: 2026-10-22 - query 6; describe 3; collections 1 - profile dev; database shop
help[3]:
- Run `mongo-axi` for live server state; `mongo-axi --help` for commands
- Run `mongo-axi collections`, `mongo-axi describe <collection>...`, `mongo-axi search <name>` or `mongo-axi sample <collection>` to explore
- "Run `mongo-axi find <collection> '{status: \"paid\"}'`, `mongo-axi aggregate <collection> '[...]'` or `mongo-axi 'db.<collection>.find({...})'`; `mongo-axi <command> --help` for flags"| Command | Applies to | Files written |
|---|---|---|
| mongo-axi setup hooks | every session of yours | ~/.claude/settings.json, ~/.codex/hooks.json, ~/.config/opencode/plugins/ |
| mongo-axi setup hooks --project | everyone working in this repository (commit the files) | .claude/settings.json, .codex/hooks.json, .opencode/plugins/; Codex also needs [features] hooks = true in ~/.codex/config.toml, which this adds |
| mongo-axi setup hooks --local | only you, only in this repository (Claude Code) | .claude/settings.local.json; it says so if git would commit the file |
- Install first: run setup from a global install (
npm install -g mongo-axi). Annpxorpnpm dlxcopy is refused: its cache is not on PATH in later sessions, so the hook would break. - Scope: run
--projectand--localfrom the repository root.setup statusandsetup uninstalltake the same flag. - Repairs: rerun
setup hooksafter you upgrade or move the install. It rewrites the hook command and is a no-op when nothing changed.setup statusshowsbrokenoroutdatedwhen a rerun is needed. Hooks installed by 0.1.0 ran the overview at session start, and a rerun switches them tocontext. - Other hooks: setup changes only its own hooks. It recognises them by the binary name and
contextargument, and leaves any other hook alone.
Skill
npx skills add cmosgh/mongo-axi --skill mongo-axi installs skills/mongo-axi/SKILL.md. It carries no live state and no per-session cost: the agent loads it when a task needs MongoDB, and it points at npx -y mongo-axi --help for current guidance. The file is generated from the CLI's own description, and CI fails when it is stale.
MIT licensed.
Other AXI tools
atl-axi: Atlassian Jira Cloud CLI for agentsbb-axi: Bitbucket Cloud CLI for agentshetzner-axi: Hetzner Cloud CLI for agentsmongo-rw-axi: Write-capable MongoDB CLI for agents, loopback hosts onlymysql-axi: Read-only MySQL CLI for agentsmysql-rw-axi: Write-capable MySQL CLI for agents, loopback hosts onlyts-axi: TypeScript symbol CLI for agents
