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 with name and data_type
  • row_count, preview_truncated, and warnings
  • sample_capped when the fetch hit the engine or LIMIT cap. Paging will not find more rows; raise the limit or add filters.
  • risk: state, severity, score, the effective gate, reasons, and plan status
  • preview: about 10 flattened rows
  • result_id for get_result_rows and summarize_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.

safe-db is proudly opensource. Apache License 2.0.

GitHub