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:
| Backend | Versions | Notes |
|---|---|---|
| MySQL | 5.7+ / 8.x | Full support including character set configuration |
| MariaDB | 10.x+ | Treated as a distinct backend with its own defaults |
| PostgreSQL | 12+ | Native protocol support via libpq-compatible driver |
| SQLite | 3.x | File-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
| Tool | MySQL / MariaDB | PostgreSQL | SQLite | Blocked when read-only |
|---|---|---|---|---|
listDatabases | Yes | Yes | No | No |
listTables | Yes | Yes | Yes | No |
listViews | Yes | Yes | Yes | No |
listTriggers | Yes | Yes | Yes | No |
listFunctions | Yes | Yes | No | No |
listProcedures | Yes | Yes | No | No |
listMaterializedViews | No | Yes | No | No |
readQuery | Yes | Yes | Yes | No |
writeQuery | Yes | Yes | Yes | Yes |
createDatabase | Yes | Yes | No | Yes |
dropDatabase | Yes | Yes | No | Yes |
dropTable | Yes | Yes | Yes | Yes |
explainQuery | Yes | Yes | Yes | No |
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-insensitiveLIKE/ILIKEpattern filtering on table name.%matches any sequence of characters,_matches a single character. Passusers%for a prefix match,%order%for a substring match. A bare word with no wildcards matches only an exact table name (PostgreSQL usesILIKE; MySQL and SQLite useLIKE, which is also ASCII-case-insensitive by default).detailed— whentrue, the response includes per-table metadata (columns, constraints, indexes, triggers) so a single call enumerates the tables and inspects them. Defaults tofalse.
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-insensitiveLIKE/ILIKEpattern filtering on view name.%matches any sequence of characters,_matches a single character. Passactive%for a prefix match,%active%for a substring match. A bare word with no wildcards matches only an exact view name. Thesearchvalue must remain identical across paginated calls for cursor continuity.detailed— whentrue, 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 tofalse.
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:
- PostgreSQL —
schema(always"public"),owner(frompg_views.viewowner),description(frompg_catalog.obj_description(c.oid, 'pg_class'), ornullwhen noCOMMENT ON VIEW), anddefinition(the SELECT body verbatim frompg_views.definition— noCREATE VIEWwrapper, no truncation). Column metadata, the redundantnamefield, and view-level options (security_barrier,security_invoker,WITH CHECK OPTION) are deliberately omitted. Column shape is recoverable from thedefinitiontext or vialistTables(detailed=true)— Postgres exposes views inpg_class, so they appear in table-style introspection too. View options are recoverable frompg_class.reloptionsviareadQuery. - MySQL / MariaDB —
schema(information_schema.VIEWS.TABLE_SCHEMA),definer(information_schema.VIEWS.DEFINER, inuser@hostform),security(information_schema.VIEWS.SECURITY_TYPE, one ofINVOKER/DEFINER),checkOption(information_schema.VIEWS.CHECK_OPTION, one ofNONE/CASCADED/LOCAL),updatable(boolean derived frominformation_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), anddefinition(the SELECT body verbatim frominformation_schema.VIEWS.VIEW_DEFINITION— noCREATE VIEWwrapper; if the connected role lacks theSHOW VIEWprivilege the engine redacts this column to the empty string but the row still appears in the response).descriptionis intentionally absent — neither MySQL nor MariaDB exposes a user-comment column for views (CREATE VIEWsyntax has noCOMMENTclause).algorithmis intentionally absent — it is a MariaDB-only column oninformation_schema.VIEWS, so reading it would fail on MySQL 9.owneris renamed todefinerto 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'snextCursorto fetch the next page.search— a case-insensitiveLIKE/ILIKEpattern filtering on trigger name.%matches any sequence of characters,_matches a single character. Passaudit%for a prefix match,%audit%for a substring match. A bare word with no wildcards matches only an exact trigger name. Thesearchvalue must remain identical across paginated calls for cursor continuity.detailed— whentrue, the response includes per-trigger metadata (timing, events, fullCREATE TRIGGERtext, and backend-specific extras) so a single call enumerates triggers and inspects them. Defaults tofalse.
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:
- PostgreSQL —
schema(always"public"),table,status(ENABLED/DISABLED/REPLICA/ALWAYS),timing(BEFORE/AFTER/INSTEAD OF),events(non-empty subset ofINSERT/UPDATE/DELETE/TRUNCATEin that fixed order),activationLevel(ROW/STATEMENT),functionName(handler function),definition(full output ofpg_get_triggerdef). - MySQL / MariaDB —
schema,table,timing(BEFORE/AFTER),events(always single-element:["INSERT"],["UPDATE"], or["DELETE"]),activationLevel(always"ROW"),definition(reconstructedCREATE DEFINER=…TRIGGER…FOR EACH ROW…), plus the session context active at trigger-creation time:sqlMode,characterSetClient,collationConnection,databaseCollation.statusandfunctionNameare absent — MySQL/MariaDB has no per-trigger enabled flag and no separate handler function (the body is inline). - SQLite —
schema(always"main"),table(may be a view name forINSTEAD OFtriggers), anddefinition(the originalCREATE TRIGGERtext fromsqlite_schema.sql, byte-for-byte). All other fields are absent — SQLite's catalogue does not exposetiming,events,activationLevel, etc. as columns; clients that need those keywords can read them off the prefix ofdefinition. Triggers whosesqlite_schema.sqlisNULL(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'snextCursorto fetch the next page.search— a case-insensitiveLIKE/ILIKEpattern filtering on function name.%matches any sequence of characters,_matches a single character. Passcalc%for a prefix match,%order%for a substring match. A bare word with no wildcards matches only an exact function name. Thesearchvalue must remain identical across paginated calls for cursor continuity.detailed— whentrue, the response includes per-function metadata (language, arguments, return type, fullCREATE FUNCTIONtext, and backend-specific extras) so a single call enumerates functions and inspects them. Defaults tofalse.
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 carriesschema,name,language,arguments,returnType,volatility(IMMUTABLE/STABLE/VOLATILE),strict,security(INVOKER/DEFINER),parallelSafety(SAFE/RESTRICTED/UNSAFE),owner,description(ornullwhen noCOMMENT ON FUNCTION), anddefinition(the fullpg_get_functiondefoutput). Brief and detailed pages share the same(proname, oid)row order, so a client can switchdetailedmid-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-separatedname typepairs frominformation_schema.PARAMETERS, empty string for zero-parameter functions),returnType(fullDTD_IDENTIFIERincluding 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(ornullwhen noCOMMENT),definition(canonical reconstructedCREATE FUNCTIONtext includingDEFINER=in`user`@`host`form), plus the session context active at function-creation time:sqlMode,characterSetClient,collationConnection,databaseCollation.volatility,parallelSafety, andstrictare absent — no MySQL/MariaDB analogues.owneris renamed todefinerto 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 asname()). Each value carriesschema,name,language,arguments,security(INVOKER/DEFINER),owner,description(ornullwhen noCOMMENT ON PROCEDURE), anddefinition(the fullpg_get_functiondefoutput forprokind='p'). Brief and detailed pages share the same(proname, oid)row order, so a client can switchdetailedmid-pagination without losing position. ThelistFunctions-only fieldsreturnType,volatility,strict, andparallelSafetyare absent — procedures don't return a value,pg_proc.provolatile/proisstrictare not user-settable for procedures, andproparallelcarries 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-separatedMODE name typetriples frominformation_schema.PARAMETERSwhereMODEisIN/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(ornullwhen noCOMMENT),definition(canonical reconstructedCREATE PROCEDUREtext includingDEFINER=in`user`@`host`form; noRETURNSclause), plus the session context active at procedure-creation time:sqlMode,characterSetClient,collationConnection,databaseCollation.returnTypeis absent (procedures have no return type);volatility,parallelSafety, andstrictare absent (no MySQL/MariaDB analogues).owneris renamed todefinerto 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'snextCursorto fetch the next page.search— a case-insensitiveILIKEpattern filtering on matview name.%matches any sequence of characters,_matches a single character. Passmv_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. Thesearchvalue must remain identical across paginated calls for cursor continuity.detailed— whentrue, the response includes per-matview metadata (schema,owner,description,definition,populated,indexed) so a single call enumerates matviews and inspects them. Defaults tofalse.
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 frompg_matviews.matviewowner.description—COMMENT ON MATERIALIZED VIEWtext, ornullwhen no comment.definition— the SELECT body verbatim frompg_matviews.definition, with noCREATE MATERIALIZED VIEWwrapper.populated—trueonce the matview has been refreshed at least once.falsefor matviews createdWITH NO DATAand never refreshed; querying such a matview returns zero rows untilREFRESH MATERIALIZED VIEWruns.indexed—truewhen at least one index exists on the matview.REFRESH MATERIALIZED VIEW CONCURRENTLYadditionally 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 NumbersROUTING_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 NumbersVAT_NUMBER— EU / UK / NI VAT identifiers (per-country length validation)
contact
PHONE_NUMBER— international phone numbers
network
IP_ADDRESS— IPv4 and IPv6 addressesMAC_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 keysJWT_TOKEN— JSON Web Tokens (headeralgfield 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.
personal—EMAIL_ADDRESSfinancial—BANK_ACCOUNT_UK,CREDIT_CARD,CVV,IBAN_CODE,ROUTING_NUMBER_US,SORT_CODE_UKgovernment—ITIN,NHS_NUMBER,NINO_UK,PASSPORT_UK,PASSPORT_US,SIN_CA,TAX_ID_EIN,US_SSN,VAT_NUMBERcontact—PHONE_NUMBERnetwork—IP_ADDRESS,MAC_ADDRESS,URLdigital-identity—API_KEY,JWT_TOKEN,PRIVATE_KEYcrypto—CRYPTO
See PII configuration for the flag, env var, and the empty-registry fallback rules.
What gets filtered
Redaction applies only to query tool output payloads — readQuery 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.combecomes<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.