Database MCP

Features

Database support, MCP tools, transport modes, and security in Database MCP

Database MCP gives AI assistants structured access to your databases through the Model Context Protocol. This page summarizes what it can do.

Database Support

Database MCP connects to four database backends with a single binary:

BackendVersionsNotes
MySQL5.7+ / 8.xFull support including character set configuration
MariaDB10.x+Treated as a distinct backend with its own defaults
PostgreSQL12+Native protocol support via libpq-compatible driver
SQLite3.xFile-based — requires only a path, no server needed

Each backend has sensible defaults for host, port, and user. See Configuration for the full default table.

MCP Tools

Database MCP exposes tools through the MCP protocol. The available tool set depends on the database backend and read-only setting. AI assistants call these tools to explore and query your database.

Tool Availability

ToolMySQL / MariaDBPostgreSQLSQLiteBlocked when read-only
listDatabasesYesYesNoNo
listTablesYesYesYesNo
listViewsYesYesYesNo
listTriggersYesYesYesNo
listFunctionsYesYesNoNo
listProceduresYesYesNoNo
listMaterializedViewsNoYesNoNo
readQueryYesYesYesNo
writeQueryYesYesYesYes
createDatabaseYesYesNoYes
dropDatabaseYesYesNoYes
dropTableYesYesYesYes
explainQueryYesYesYesNo

SQLite is file-based with a single database, so listDatabases, createDatabase, dropDatabase, listFunctions, listProcedures, and listMaterializedViews are not applicable. listMaterializedViews is PostgreSQL-only — MySQL and MariaDB do not have a materialized-view feature.

Cursor Pagination

All schema-discovery tools (listDatabases, listTables, listViews, listTriggers, listFunctions, listProcedures, listMaterializedViews) and readQuery (for SELECT results) return one page at a time. Each paginated response carries up to the configured page size (default 100; see --db-page-size for how to change it) plus, when more items remain, a nextCursor string alongside the result data.

To walk the full result, call the tool again with the previous response's nextCursor as the new cursor argument. Iteration ends when a response omits nextCursor. An empty first page with no nextCursor is also a valid terminal response.

Cursors are opaque tokens. Treat them as black-box strings — do not parse, modify, or persist them across server restarts. Passing a malformed cursor returns a JSON-RPC error (code -32602); the server does not silently restart from the beginning.

Only SELECT statements in readQuery paginate. SHOW, DESCRIBE / DESC, USE, and EXPLAIN always return a single page and ignore any cursor argument.

listDatabases

Discover accessible databases on the connected server. This is typically the first tool an assistant calls to understand what data is available. Results are paginated — see Cursor Pagination for the protocol. Not available for SQLite.

listTables

List tables in a database, one page at a time. Results are paginated — see Cursor Pagination for the protocol. On MySQL/MariaDB and PostgreSQL the database parameter selects which database to inspect (defaults to the active database); SQLite operates on a single file and has no database parameter.

The tool accepts two optional parameters that shape the response:

  • search — a case-insensitive LIKE/ILIKE pattern filtering on table name. % matches any sequence of characters, _ matches a single character. Pass users% for a prefix match, %order% for a substring match. A bare word with no wildcards matches only an exact table name (PostgreSQL uses ILIKE; MySQL and SQLite use LIKE, which is also ASCII-case-insensitive by default).
  • detailed — when true, the response includes per-table metadata (columns, constraints, indexes, triggers) so a single call enumerates the tables and inspects them. Defaults to false.

In brief mode (default) the response shape is:

{
  "entries": ["customers", "orders", "products"],
  "nextCursor": "<opaque or absent>"
}

In detailed mode the response is keyed by table name:

{
  "entries": {
    "orders": {
      "schema": "public",
      "kind": "TABLE",
      "owner": "app",
      "comment": null,
      "columns": [
        { "name": "id", "dataType": "bigint", "ordinalPosition": 1, "nullable": false, "default": null, "comment": null }
      ],
      "constraints": [
        { "name": "pk", "type": "PRIMARY KEY", "columns": ["id"], "definition": "PRIMARY KEY (id)" }
      ],
      "indexes": [],
      "triggers": []
    }
  },
  "nextCursor": null
}

Field availability follows what the backend exposes — owner is null on SQLite and MariaDB; PostgreSQL fills schema from the catalog while SQLite uses main.

listViews

List the views defined in a database. Materialized views are not returned — they are exposed via listMaterializedViews. Results are paginated — see Cursor Pagination for the protocol. On MySQL/MariaDB and PostgreSQL the database parameter selects which database to inspect (defaults to the active database); SQLite operates on a single file and has no database parameter. PostgreSQL restricts the listing to the public schema.

In brief mode (default) the response is a sorted JSON array of view-name strings. View names are unique per schema, so no duplicates appear:

{
  "entries": ["active_users", "monthly_revenue"],
  "nextCursor": "<opaque or absent>"
}

MySQL/MariaDB and PostgreSQL accept these optional parameters; SQLite returns the brief shape only:

  • search — a case-insensitive LIKE/ILIKE pattern filtering on view name. % matches any sequence of characters, _ matches a single character. Pass active% for a prefix match, %active% for a substring match. A bare word with no wildcards matches only an exact view name. The search value must remain identical across paginated calls for cursor continuity.
  • detailed — when true, the response includes per-view metadata (definer/owner, comment where supported, full SELECT body, plus backend-specific extras) so a single call enumerates views and inspects them. Defaults to false.

In detailed mode the response is keyed by bare view name — views cannot overload, so no signature suffix is needed. The view name is the map key only; it is not repeated inside the value. Brief and detailed pages share the same viewname row order, so a client can switch detailed mid-pagination without losing position.

The PostgreSQL payload carries owner and comment plus the SELECT body:

{
  "entries": {
    "active_users": {
      "schema": "public",
      "owner": "app_user",
      "description": "Currently-active user accounts",
      "definition": " SELECT id,\n    name,\n    email\n   FROM users\n  WHERE active = true;"
    }
  },
  "nextCursor": null
}

The MySQL/MariaDB payload carries definer / security / check-option / updatability / session-context fields plus the SELECT body:

{
  "entries": {
    "active_users": {
      "schema": "app",
      "definer": "root@%",
      "security": "DEFINER",
      "checkOption": "NONE",
      "updatable": true,
      "characterSetClient": "utf8mb4",
      "collationConnection": "utf8mb4_general_ci",
      "definition": "select `app`.`users`.`id` AS `id`,`app`.`users`.`name` AS `name`,`app`.`users`.`email` AS `email` from `app`.`users`"
    }
  },
  "nextCursor": null
}

The detailed-mode field set diverges per backend:

  • PostgreSQLschema (always "public"), owner (from pg_views.viewowner), description (from pg_catalog.obj_description(c.oid, 'pg_class'), or null when no COMMENT ON VIEW), and definition (the SELECT body verbatim from pg_views.definition — no CREATE VIEW wrapper, no truncation). Column metadata, the redundant name field, and view-level options (security_barrier, security_invoker, WITH CHECK OPTION) are deliberately omitted. Column shape is recoverable from the definition text or via listTables(detailed=true) — Postgres exposes views in pg_class, so they appear in table-style introspection too. View options are recoverable from pg_class.reloptions via readQuery.
  • MySQL / MariaDBschema (information_schema.VIEWS.TABLE_SCHEMA), definer (information_schema.VIEWS.DEFINER, in user@host form), security (information_schema.VIEWS.SECURITY_TYPE, one of INVOKER / DEFINER), checkOption (information_schema.VIEWS.CHECK_OPTION, one of NONE / CASCADED / LOCAL), updatable (boolean derived from information_schema.VIEWS.IS_UPDATABLE = 'YES'), characterSetClient (information_schema.VIEWS.CHARACTER_SET_CLIENT, the session character set captured at view creation), collationConnection (information_schema.VIEWS.COLLATION_CONNECTION, the session connection collation captured at view creation), and definition (the SELECT body verbatim from information_schema.VIEWS.VIEW_DEFINITION — no CREATE VIEW wrapper; if the connected role lacks the SHOW VIEW privilege the engine redacts this column to the empty string but the row still appears in the response). description is intentionally absent — neither MySQL nor MariaDB exposes a user-comment column for views (CREATE VIEW syntax has no COMMENT clause). algorithm is intentionally absent — it is a MariaDB-only column on information_schema.VIEWS, so reading it would fail on MySQL 9. owner is renamed to definer to match the engine's terminology.

SQLite returns the brief shape only — search and detailed are not accepted there.

listTriggers

List user-defined triggers on tables in a database. Internal constraint, foreign-key, and sqlite_* triggers are excluded. Results are paginated — see Cursor Pagination for the protocol. On MySQL/MariaDB and PostgreSQL the database parameter selects which database to inspect (defaults to the active database); SQLite operates on a single file and has no database parameter. PostgreSQL restricts the listing to the public schema.

The tool accepts these optional parameters:

  • cursor — opaque pagination token; pass back the prior response's nextCursor to fetch the next page.
  • search — a case-insensitive LIKE/ILIKE pattern filtering on trigger name. % matches any sequence of characters, _ matches a single character. Pass audit% for a prefix match, %audit% for a substring match. A bare word with no wildcards matches only an exact trigger name. The search value must remain identical across paginated calls for cursor continuity.
  • detailed — when true, the response includes per-trigger metadata (timing, events, full CREATE TRIGGER text, and backend-specific extras) so a single call enumerates triggers and inspects them. Defaults to false.

In brief mode (default) the response shape is:

{
  "entries": ["customers_audit_after_insert", "orders_audit_after_insert", "set_updated_at"],
  "nextCursor": "<opaque or absent>"
}

In detailed mode the response is keyed by trigger name. The PostgreSQL payload is the richest:

{
  "entries": {
    "orders_audit_after_iu": {
      "schema": "public",
      "table": "orders",
      "status": "ENABLED",
      "timing": "AFTER",
      "events": ["INSERT", "UPDATE"],
      "activationLevel": "ROW",
      "functionName": "audit_orders",
      "definition": "CREATE TRIGGER orders_audit_after_iu AFTER INSERT OR UPDATE ON public.orders FOR EACH ROW EXECUTE FUNCTION public.audit_orders()"
    }
  },
  "nextCursor": null
}

The detailed-mode field set diverges per backend:

  • PostgreSQLschema (always "public"), table, status (ENABLED / DISABLED / REPLICA / ALWAYS), timing (BEFORE / AFTER / INSTEAD OF), events (non-empty subset of INSERT / UPDATE / DELETE / TRUNCATE in that fixed order), activationLevel (ROW / STATEMENT), functionName (handler function), definition (full output of pg_get_triggerdef).
  • MySQL / MariaDBschema, table, timing (BEFORE / AFTER), events (always single-element: ["INSERT"], ["UPDATE"], or ["DELETE"]), activationLevel (always "ROW"), definition (reconstructed CREATE DEFINER=TRIGGERFOR EACH ROW …), plus the session context active at trigger-creation time: sqlMode, characterSetClient, collationConnection, databaseCollation. status and functionName are absent — MySQL/MariaDB has no per-trigger enabled flag and no separate handler function (the body is inline).
  • SQLiteschema (always "main"), table (may be a view name for INSTEAD OF triggers), and definition (the original CREATE TRIGGER text from sqlite_schema.sql, byte-for-byte). All other fields are absent — SQLite's catalogue does not expose timing, events, activationLevel, etc. as columns; clients that need those keywords can read them off the prefix of definition. Triggers whose sqlite_schema.sql is NULL (rare; extension-generated rows or hand-edited catalogues) are silently omitted from detailed mode but still listed by name in brief mode.

listFunctions

List user-defined SQL functions in a database. PostgreSQL excludes aggregates, window functions, and procedures; MySQL/MariaDB excludes loadable UDFs registered in mysql.func. Results are paginated — see Cursor Pagination for the protocol. The database parameter selects which database to inspect and defaults to the active database. PostgreSQL restricts the listing to the public schema. Available on MySQL/MariaDB and PostgreSQL only — not available for SQLite.

The tool accepts these optional parameters:

  • cursor — opaque pagination token; pass back the prior response's nextCursor to fetch the next page.
  • search — a case-insensitive LIKE/ILIKE pattern filtering on function name. % matches any sequence of characters, _ matches a single character. Pass calc% for a prefix match, %order% for a substring match. A bare word with no wildcards matches only an exact function name. The search value must remain identical across paginated calls for cursor continuity.
  • detailed — when true, the response includes per-function metadata (language, arguments, return type, full CREATE FUNCTION text, and backend-specific extras) so a single call enumerates functions and inspects them. Defaults to false.

In brief mode (default) the response is a sorted JSON array of function-name strings. PostgreSQL overloads appear as one entry per overload — duplicate name strings are expected:

{
  "entries": ["calc_order_subtotal", "calc_order_total", "calc_order_total"],
  "nextCursor": null
}

In detailed mode the response is keyed by function signature. The PostgreSQL payload disambiguates overloads via name(arguments):

{
  "entries": {
    "calc_order_total(order_id integer, tax_rate numeric)": {
      "schema": "public",
      "name": "calc_order_total",
      "language": "sql",
      "arguments": "order_id integer, tax_rate numeric",
      "returnType": "numeric",
      "volatility": "IMMUTABLE",
      "strict": true,
      "security": "INVOKER",
      "parallelSafety": "SAFE",
      "owner": "app_user",
      "description": "Sums line items minus discounts",
      "definition": "CREATE OR REPLACE FUNCTION public.calc_order_total(order_id integer, tax_rate numeric) RETURNS numeric ..."
    }
  },
  "nextCursor": null
}

The detailed-mode field set diverges per backend:

  • PostgreSQL — keyed by name(arguments) (overloads disambiguate). Each value carries schema, name, language, arguments, returnType, volatility (IMMUTABLE / STABLE / VOLATILE), strict, security (INVOKER / DEFINER), parallelSafety (SAFE / RESTRICTED / UNSAFE), owner, description (or null when no COMMENT ON FUNCTION), and definition (the full pg_get_functiondef output). Brief and detailed pages share the same (proname, oid) row order, so a client can switch detailed mid-pagination without losing position.
  • MySQL / MariaDB — keyed by bare function name (the engines do not allow function overloading). Each value carries schema, language (typically "SQL"; MariaDB external-language functions report the external language name), arguments (comma-separated name type pairs from information_schema.PARAMETERS, empty string for zero-parameter functions), returnType (full DTD_IDENTIFIER including length/precision/unsigned/enum/set members), deterministic (boolean), sqlDataAccess (CONTAINS_SQL / NO_SQL / READS_SQL_DATA / MODIFIES_SQL_DATA), security (INVOKER / DEFINER), definer (user@host), description (or null when no COMMENT), definition (canonical reconstructed CREATE FUNCTION text including DEFINER= in `user`@`host` form), plus the session context active at function-creation time: sqlMode, characterSetClient, collationConnection, databaseCollation. volatility, parallelSafety, and strict are absent — no MySQL/MariaDB analogues. owner is renamed to definer to match the engine's terminology.

listProcedures

List user-defined stored procedures in a database, optionally filtered by search substring and/or expanded with detailed: true. Available on MySQL/MariaDB and PostgreSQL (public schema, PostgreSQL 11+). Not available for SQLite. Results are paginated — see Cursor Pagination for the protocol. The search value (case-insensitive LIKE/ILIKE substring; % matches any sequence, _ a single character) must remain identical across paginated calls for cursor continuity.

In brief mode (default) the response is a sorted JSON array of procedure-name strings. PostgreSQL overloads appear as one entry per overload — duplicate name strings are expected:

{
  "entries": ["archive_order", "archive_order_history", "archive_order_history"],
  "nextCursor": null
}

In detailed mode the response is keyed by procedure signature. The PostgreSQL payload disambiguates overloads via name(arguments); zero-arg procedures key as name() for shape uniformity:

{
  "entries": {
    "archive_order(order_id integer)": {
      "schema": "public",
      "name": "archive_order",
      "language": "plpgsql",
      "arguments": "order_id integer",
      "security": "INVOKER",
      "owner": "app_user",
      "description": "Moves an order into the archive table",
      "definition": "CREATE OR REPLACE PROCEDURE public.archive_order(order_id integer) ..."
    }
  },
  "nextCursor": null
}

The detailed-mode field set diverges per backend:

  • PostgreSQL — keyed by name(arguments) (overloads disambiguate; zero-arg procedures key as name()). Each value carries schema, name, language, arguments, security (INVOKER / DEFINER), owner, description (or null when no COMMENT ON PROCEDURE), and definition (the full pg_get_functiondef output for prokind='p'). Brief and detailed pages share the same (proname, oid) row order, so a client can switch detailed mid-pagination without losing position. The listFunctions-only fields returnType, volatility, strict, and parallelSafety are absent — procedures don't return a value, pg_proc.provolatile/proisstrict are not user-settable for procedures, and proparallel carries no procedure-level guarantee.
  • MySQL / MariaDB — keyed by bare procedure name (the engines do not allow procedure overloading). Each value carries schema, language (typically "SQL"; MariaDB external-language procedures report the external language name), arguments (comma-separated MODE name type triples from information_schema.PARAMETERS where MODE is IN/OUT/INOUT; empty string for zero-parameter procedures), deterministic (boolean), sqlDataAccess (CONTAINS_SQL / NO_SQL / READS_SQL_DATA / MODIFIES_SQL_DATA), security (INVOKER / DEFINER), definer (user@host), description (or null when no COMMENT), definition (canonical reconstructed CREATE PROCEDURE text including DEFINER= in `user`@`host` form; no RETURNS clause), plus the session context active at procedure-creation time: sqlMode, characterSetClient, collationConnection, databaseCollation. returnType is absent (procedures have no return type); volatility, parallelSafety, and strict are absent (no MySQL/MariaDB analogues). owner is renamed to definer to match the engine's terminology.

listMaterializedViews

List materialized views in the public schema of a PostgreSQL database. Unlike regular views, materialized views store their results physically and must be refreshed explicitly. Regular views and system-schema matviews are excluded. Results are paginated — see Cursor Pagination for the protocol. PostgreSQL only — not available for MySQL, MariaDB, or SQLite.

The tool accepts these optional parameters:

  • cursor — opaque pagination token; pass back the prior response's nextCursor to fetch the next page.
  • search — a case-insensitive ILIKE pattern filtering on matview name. % matches any sequence of characters, _ matches a single character. Pass mv_orders% for a prefix match, %orders% for a substring match. A bare word with no wildcards matches only an exact matview name. SQL meta-characters (', ;, --) are bound as parameter values and never interpolated. The search value must remain identical across paginated calls for cursor continuity.
  • detailed — when true, the response includes per-matview metadata (schema, owner, description, definition, populated, indexed) so a single call enumerates matviews and inspects them. Defaults to false.

In brief mode (default) the response is a sorted JSON array of matview-name strings. Matview names are unique per schema, so no duplicates appear:

{
  "entries": ["mv_archived_orders", "mv_orders_by_region", "mv_recent_orders"],
  "nextCursor": null
}

In detailed mode the response is keyed by bare matview name — matviews cannot overload, so no signature suffix is needed. The matview name is the map key only; it is not repeated inside the value. Brief and detailed pages share the same matviewname row order, so a client can switch detailed mid-pagination without losing position:

{
  "entries": {
    "mv_orders_by_region": {
      "schema": "public",
      "owner": "app_user",
      "description": "Orders rolled up by region for the BI dashboard.",
      "definition": " WITH paid_orders AS (\n         SELECT orders.id, orders.customer_id, orders.total\n           FROM orders\n          WHERE orders.status = 'paid'\n        )\n SELECT customer_id AS region, count(*) AS order_count, sum(total) AS gross\n   FROM paid_orders\n  GROUP BY customer_id;",
      "populated": true,
      "indexed": true
    }
  },
  "nextCursor": null
}

Per-entry fields (sourced from pg_matviews joined to pg_class for the OID needed by obj_description):

  • schema — schema name (always "public" in this build).
  • owner — owning role's name from pg_matviews.matviewowner.
  • descriptionCOMMENT ON MATERIALIZED VIEW text, or null when no comment.
  • definition — the SELECT body verbatim from pg_matviews.definition, with no CREATE MATERIALIZED VIEW wrapper.
  • populatedtrue once the matview has been refreshed at least once. false for matviews created WITH NO DATA and never refreshed; querying such a matview returns zero rows until REFRESH MATERIALIZED VIEW runs.
  • indexedtrue when at least one index exists on the matview. REFRESH MATERIALIZED VIEW CONCURRENTLY additionally requires a unique index; this tool reports the broader has-any-index signal.

Detailed mode deliberately omits column metadata, tablespace, storage parameters, and unique-index detection. Column shape is recoverable from the definition text or via listTables(detailed=true) since Postgres exposes matviews in pg_class. Unique-index detection requires a pg_index join — advanced callers can readQuery pg_indexes for that.

readQuery

Execute a read-only SQL query (SELECT, SHOW, DESCRIBE, USE, EXPLAIN) against a specific database. This tool is always available and enforces SQL validation — write statements are rejected even when read-only mode is disabled. See Security for details on query validation. SELECT results are paginated — see Cursor Pagination for the protocol. SHOW, DESCRIBE, USE, and EXPLAIN always return a single page and ignore any cursor argument.

writeQuery

Execute a write SQL query (INSERT, UPDATE, DELETE, CREATE, ALTER, DROP) against a specific database. This tool is only available when read-only mode is disabled. No SQL type validation is performed — the tool boundary is the access control.

createDatabase

Create a new database. Only available when read-only mode is disabled. Not supported for SQLite.

dropDatabase

Drop an existing database. Refuses to drop the currently connected database. Only available when read-only mode is disabled. Not supported for SQLite.

dropTable

Drop a table from a database. If the table has foreign key dependents, the database error is surfaced directly — no silent failures. On PostgreSQL, a cascade parameter is available to force the drop using CASCADE, which also removes dependent foreign key constraints. Only available when read-only mode is disabled.

explainQuery

Return the execution plan for a SQL query. Supports an optional analyze parameter for actual execution statistics on PostgreSQL and MySQL/MariaDB. In read-only mode, EXPLAIN ANALYZE is only allowed for read-only statements (SELECT, SHOW) since it actually executes the query. SQLite always uses EXPLAIN QUERY PLAN and does not support ANALYZE. Always available regardless of read-only mode.

Transport Modes

Database MCP supports two transport modes for communicating with MCP clients.

Stdio

The server communicates over standard input/output. This mode works with local MCP clients like Claude Desktop, Claude Code, and Cursor. No network configuration is needed — the client launches the server as a subprocess and passes the stdio subcommand.

Best for: Local development, single-user setups, desktop MCP clients.

HTTP

The server runs as an HTTP service with Streamable HTTP transport, CORS preflight, and server-side Origin/Host header allowlists. This mode is useful for remote access or shared environments where multiple clients connect to the same server.

Best for: Remote servers, shared team databases, environments where the MCP client cannot launch local processes.

See Configuration for HTTP-specific options like bind host, port, and allowed origins.

PII Redaction

Database MCP ships an optional PII redactor that rewrites detected sensitive spans inside query tool output before the result reaches the AI assistant. The feature is off by default; enable it per-server via --pii / PII_ENABLE, and select how spans are rewritten via --pii-operator / PII_OPERATOR. See PII configuration for the full flag reference.

What gets detected

Twenty-four built-in regex recognisers ship by default, grouped into seven categories. Each recogniser is identified by an entity type that drives the replace operator's placeholder and is tagged with a category for opt-in subsetting via --pii-categories / PII_CATEGORIES. Several recognisers ship with a checksum or context validator (Luhn, mod-11, ABA, EIN prefix, JWT header, PEM type-match, IRS ITIN range, keyword context) that promotes the match to maximum confidence and rejects look-alikes.

personal

  • EMAIL_ADDRESS — email addresses

financial

  • BANK_ACCOUNT_UK — UK bank account numbers (8–10 digits, keyword-gated)
  • CREDIT_CARD — credit card numbers (Luhn-validated)
  • CVV — card verification values (3–4 digits, keyword-gated)
  • IBAN_CODE — International Bank Account Numbers
  • ROUTING_NUMBER_US — US ABA routing transit numbers (checksum + keyword-gated)
  • SORT_CODE_UK — UK sort codes (keyword-gated)

government

  • ITIN — US Individual Taxpayer Identification Numbers (IRS range-validated)
  • NHS_NUMBER — UK NHS patient identifiers (mod-11 checksum)
  • NINO_UK — UK National Insurance Numbers (HMRC prefix blocklist)
  • PASSPORT_UK — UK passport numbers (9 digits, keyword-gated)
  • PASSPORT_US — US passport numbers (keyword-gated)
  • SIN_CA — Canadian Social Insurance Numbers (Luhn-validated)
  • TAX_ID_EIN — US Employer Identification Numbers (IRS prefix-validated)
  • US_SSN — United States Social Security Numbers
  • VAT_NUMBER — EU / UK / NI VAT identifiers (per-country length validation)

contact

  • PHONE_NUMBER — international phone numbers

network

  • IP_ADDRESS — IPv4 and IPv6 addresses
  • MAC_ADDRESS — MAC addresses (colon or dash separated)
  • URL — http / https URLs

digital-identity

  • API_KEY — five named provider tokens (AWS access, GitHub PAT, Stripe live, Google API, OpenAI) plus keyword-gated AWS secret access keys
  • JWT_TOKEN — JSON Web Tokens (header alg field validated; signature not verified)
  • PRIVATE_KEY — PEM-fenced private key blocks (BEGIN / END type must match)

crypto

  • CRYPTO — Bitcoin and similar wallet addresses

Categories

Each built-in recogniser belongs to one category. Operators can scope the analyzer registry to a subset by listing categories on --pii-categories / PII_CATEGORIES; leaving the flag unset enables every built-in.

  • personalEMAIL_ADDRESS
  • financialBANK_ACCOUNT_UK, CREDIT_CARD, CVV, IBAN_CODE, ROUTING_NUMBER_US, SORT_CODE_UK
  • governmentITIN, NHS_NUMBER, NINO_UK, PASSPORT_UK, PASSPORT_US, SIN_CA, TAX_ID_EIN, US_SSN, VAT_NUMBER
  • contactPHONE_NUMBER
  • networkIP_ADDRESS, MAC_ADDRESS, URL
  • digital-identityAPI_KEY, JWT_TOKEN, PRIVATE_KEY
  • cryptoCRYPTO

See PII configuration for the flag, env var, and the empty-registry fallback rules.

What gets filtered

Redaction applies only to query tool output payloadsreadQuery results today, and any future query tools that route through the same redactor. Logs, error messages, schema-discovery responses (listTables, listViews, etc.), and tool arguments are not affected.

How spans are rewritten

The configured operator decides how each detected span is replaced:

  • replace (default) — entity-aware placeholder. For example, jane.doe@example.com becomes <EMAIL_ADDRESS>. Plan for this when downstream code parses query output: a column that previously held a plain email now holds the placeholder string for matching rows.
  • mask — length-preserving * characters.
  • redact — span removed (replaced with empty string).
  • hash — stable hex digest derived via SHA-256.

Nested JSON values

Redaction walks every string leaf in json and jsonb columns (and any other column the backend hands the server as a structured serde_json::Value::Object or Value::Array). A PII string redacts identically whether it arrives as a plain text column, a JSON literal cast to text, or a leaf nested inside a jsonb value at any depth. Non-string scalars (numbers, booleans, null) and the JSON shape (key names, array indexes, container ordering) are preserved.

JSON object keys are never inspected or modified — only values are redacted. A row like {"jane.doe@example.com": "value"} returns with the key intact. If you need key redaction, please open an issue; today's contract is values-only.

See PII configuration for the toggle, env var, and operator-selection details.

Single Binary

Database MCP ships as a single compiled binary with no runtime dependencies. There is no language runtime to install, no package manager to configure, and no dependency tree to manage. Download the binary for your platform and it is ready to use.

On this page