MCP server
Install with npm, add a connection from the CLI, and let an agent inspect schema and run one gated SELECT.
@safe-db/mcp is a stdio MCP server. Agents use it to inspect schema and run a
single SELECT through the same parser, validator, compiler, risk gate, and row and time
caps as the desktop app. It does not reimplement JDBC, validation, or gating; it maps tools onto
the shared engine. Accepted and rejected SQL are the same as on the SQL screen, and the caps are the ones in Limits & risk model.
Install
Install with npm. You need Node.js 18 or newer for npx; the package bundles a
Temurin 25 runtime, so the machine does not need Java. The first command also adds a connection:
npx -y @safe-db/mcp setup --dialect mysql --database app --username readonly --password-file /absolute/path
On a terminal, setup prompts for any missing field and reads the password without
echo. Off a terminal it needs --password-file: an absolute path to an owner-only
file holding one line. Setup tests the connection, then saves it. The secret goes to the
credential store, not to any file the agent reads.
Client configuration
Add the server to your MCP client the way it expects. The entry is the same everywhere:
{
"mcpServers": {
"safe-db": {
"command": "npx",
"args": ["-y", "@safe-db/mcp"]
}
}
} With no arguments the binary speaks MCP JSON-RPC on stdin and stdout. Logs go to stderr. There
is nothing to put in the config beyond the command: no host, no user, no database, and no
password. Environment variables are not read for any of those either. If you installed the
package globally with npm install -g, the command is safe-db-mcp with no
arguments.
Platforms
macOS on Apple Silicon (not Intel), Windows x64, and glibc Linux on x64 and arm64. Alpine and other musl distributions are not supported. The desktop app still does not run on Linux; the server does.
The server does not load a desktop launch profile. TLS is set per connection with --transport. Oracle over TCPS still needs --oracle-wallet.
Data directory and credentials
Connection profiles without passwords live in connections.json. Passwords never go
in that file, in the client config, in tool arguments, or in environment variables. Where the
rest lives depends on the platform:
- Windows
- %APPDATA%\com.safedb.app, shared with the desktop app. Passwords in Credential Manager, under the same entry the app uses.
- macOS
- ~/Library/Application Support/com.safedb.mcp. Passwords in owner-only files under credentials/. Keychain is not opened and desktop passwords are not reused.
- Linux
- $XDG_DATA_HOME/com.safedb.mcp, or ~/.local/share/com.safedb.mcp. Same file store as macOS.
So on Windows, list_connections returns the same list the app shows. On macOS the server
keeps a separate store; do not expect an agent to reach connections you saved in the app.
The risk gate and blocked schemas come from settings.json in the same directory, with
the gate defaulting to Standard. No tool changes them. On Windows the desktop Settings screen writes
that file; on macOS and Linux the server has its own.
CLI
The binary is safe-db-mcp. Connections are added by a person at the CLI, not by
tools, and passwords are never flags: --password and -p are rejected.
safe-db-mcp safe-db-mcp setup safe-db-mcp connections add [options] safe-db-mcp connections list safe-db-mcp connections delete <id>
setup and connections add are the same command. Its flags:
- --name
- Defaults to the database name.
- --dialect
- postgres, mysql, mssql, or oracle.
- --host
- Defaults to localhost.
- --port
- The dialect default when omitted.
- --database
- Required.
- --username
- Required.
- --password-file
- Absolute path to an owner-only file with one UTF-8 line. Setup copies the secret into the credential store.
- --transport
- disabled, encrypt-only, verify-ca, or verify-identity. Off a terminal, defaults from the host location.
- --oracle-wallet
- Absolute path. Required for Oracle when transport is not disabled.
Tools
A progressive catalog, then a gated query. No tool returns the full schema or the full result,
and no tool accepts a password or a URL with embedded credentials. At startup the server tells
the client to begin with list_connections and pass its id as connection_id.
- list_connections
- id, name, dialect, and database for each saved connection. No secrets.
- list_tables
- schema, name, qualified_name, size_class, and column_count for visible tables. Cached for 5 minutes; pass refresh=true to introspect again. Blocked schemas and system catalogs are omitted.
- describe_table
- Columns (name, data_type, nullable), indexes, and foreign keys for one table named by schema and table from list_tables. Unknown or blocked tables return not_found.
- run_query
- connection_id and sql, plus default_schema for unqualified table names. Runs one SELECT through the engine and returns a receipt, not the grid. When the plan or its cost is unavailable, the first call returns confirmation_required; the client shows the user, then retries with the confirmation object.
- get_result_rows
- Pages the fetched sample by result_id. offset defaults to 0; limit defaults to 50 and is capped at 50.
- summarize_result
- Per-column null_count, min and max, and up to 8 distinct values from the same sample.
- delete_connection
- Deletes a saved connection by id. The first call returns confirmation_required with reasons and a confirmation object; the client shows the user, then retries with that object.
The run_query receipt
A successful run returns JSON with:
columns, each withnameanddata_typerow_count,preview_truncated, andwarningssample_cappedwhen the fetch hit the engine orLIMITcap. Paging will not find more rows; raise the limit or add filters.risk: state, severity, score, the effective gate, reasons, and plan statuspreview: about 10 flattened rowsresult_idforget_result_rowsandsummarize_result
The engine still fetches up to the row cap; the receipt just does not carry it. Results stay in
the server process for 30 minutes, at most 16 of them, then expire. Each one is also written as
an owner-only file under results/ in the data directory, deleted when the result expires
and wiped when the server starts. Paging runs no second query.
Errors
Every tool error is JSON with error, message, and warnings. The codes are invalid_arguments, not_found, parse, validation, compilation, execution, risk_gate, confirmation_required, and internal. A risk_gate error carries the risk summary; the fix is a different query or a
different gate in settings, not a retry. A confirmation_required error, from run_query or delete_connection, carries reasons and a confirmation
object; show the reasons to the user, then retry the same tool with that object. The client does
not invent one. A parse error on an unqualified table name usually wants default_schema. Aggregates are not parsed; run the base query and use summarize_result.
Not in this version
- Sharing macOS Keychain items with the signed desktop app
- Tools that add or update a connection, or change settings
- Environment variables for host, user, database, or password
- Dumping the full schema or full result into tool JSON
- MCP resources, elicitation, catalog search, and CSV export
Internals, constants, and the design contract are in the repository’s docs/mcp.md.