Search

Search the documentation

Protobox

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

The 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-rows at save time, up to 1000). A capped response reports truncated: true so 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);

What's Next

Tools

Create custom functions clients can call over MCP

Knowledge Base

Upload docs and FAQs for semantic search

On this page