Databases
Connect a private Postgres or MongoDB source as a read-only tool — saved, parametrised queries any assistant can call
Databases
Your business data lives behind a firewall, in a database no public assistant can reach. Databases connect that source as read-only, then turn a specific question — "this customer's orders", "open tickets by priority" — into a saved query. Save it once, and it becomes a tool: db_<name>, callable over MCP exactly like any other tool.
There is no free-form SQL tool. Every saved query is checked at save time (a single SELECT, or a Mongo find/aggregate without $out/$merge/$function) and runs read-only at execution time (a read-only transaction, a statement timeout, and a row cap) — read-only by construction, not by convention.
Connect a database
protobox db add orders-db --url "postgres://user:pass@host:5432/orders"✓ Connected "orders-db" (postgres)
ℹ 3 table(s)/collection(s): orders, customers, line_items
ℹ Save a query: protobox db query add orders-db <query-name> --sql "SELECT ..." --param key:stringThe connection string is tested immediately — a bad host, a bad password, or an unreachable network all fail here, before anything is stored. Once connected, the connection string is stored in the platform's existing secrets store; protobox db list and every other read never returns it.
MongoDB works the same way: protobox db add support-db --url "mongodb+srv://user:pass@cluster/support".
Save a query
protobox db query add orders-db orders_by_customer \
--sql "SELECT id, total_cents, created_at FROM orders WHERE customer_id = :customer_id ORDER BY created_at DESC" \
--param customer_id:string✓ Saved query "orders_by_customer" → tool db_orders_by_customer
ℹ Run it over MCP as db_orders_by_customer, or: protobox db query list orders-db:paramName placeholders in the SQL become the tool's typed parameters — --param customer_id:string (append :optional to make a param not required, e.g. --param limit:number:optional). For a MongoDB database, pass --collection and --pipeline (a JSON aggregation pipeline) instead of --sql, with "{{paramName}}" as the placeholder inside the pipeline.
A query that isn't a single read-only statement is rejected right here, at save time — never silently saved as something else:
protobox db query add orders-db wipe --sql "DELETE FROM orders" --param x:string✗ The platform rejected the input
WRITE_STATEMENT_REJECTED: only SELECT statements are allowed.Run it
The saved query is now the tool db_orders_by_customer — any MCP client with access to it calls it directly, passing customer_id. Three things are enforced on every call, not just at save time:
- Row cap — 200 rows by default (
--max-rowsat save time, up to 1000). A capped response reportstruncated: trueso the caller knows there was more. - Statement timeout — 10 seconds.
- Read-only transaction (Postgres) —
BEGIN TRANSACTION READ ONLY, so a statement that only looked read-only cannot write, even by accident.
When the source goes away
Removing a database — protobox db remove <id>, or revoking its stored credential — does not delete the tools its queries created. They stay listed, and fail closed on the next call:
Error: DATABASE_UNAVAILABLE: the credential for "orders-db" has been revoked. Reconnect the database to restore this tool.Every call — success or failure — runs through the same audited, metered execution path as any other tool. A revoked credential shows up as a failed execution in the tool's history, not a silent gap.
SDK
import { ProtoboxSDK } from '@protoboxai/sdk';
const sdk = new ProtoboxSDK({ apiKey: process.env.PROTOBOX_API_KEY! });
const { engine, tables } = await sdk.databases.add({ name: 'orders-db', url: process.env.ORDERS_DB_URL! });
const query = await sdk.databases.addQuery(database.id, {
name: 'orders_by_customer',
sql: 'SELECT id, total_cents FROM orders WHERE customer_id = :customer_id',
params: [{ name: 'customer_id', type: 'string' }],
});
// query.toolId — the materialized db_orders_by_customer tool
const databases = await sdk.databases.list(); // never includes the connection string
await sdk.databases.remove(database.id);