Blog /
Connect Claude Desktop to your PostgreSQL database with MCP
Published . Written for nlqueries-core 0.4.
mcp postgresql claude-desktop
Connecting Claude Desktop to a PostgreSQL database over MCP takes a few
commands and one JSON entry. This post walks through it with nlqueries-core
0.4, where the MCP server's query tool generates SQL, runs it read-only and
returns the rows. The order matters: the database role comes first, then the
connection, then the knowledge base, and only then Claude Desktop. The last
section covers what changes when the server is used by more than one person.
Step 1: a role that cannot write
Claude will be asking your database questions on your behalf. Whatever the model writes, the login it runs as decides what can actually happen. So start with a dedicated PostgreSQL role that owns nothing and can only read what you mean to expose.
Hardening the database has the full script in six steps. The short version:
CREATE ROLE nlqueries_readonly;
CREATE ROLE nlqueries LOGIN PASSWORD 'use-a-generated-secret' IN ROLE nlqueries_readonly;
ALTER ROLE nlqueries
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
GRANT CONNECT ON DATABASE mydb TO nlqueries_readonly;
GRANT USAGE ON SCHEMA analytics TO nlqueries_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA analytics TO nlqueries_readonly;
ALTER ROLE nlqueries SET statement_timeout = '30s';
ALTER ROLE nlqueries SET default_transaction_read_only = on;
The full version also revokes TEMPORARY and CREATE, removes EXECUTE on
functions from PUBLIC, fixes the search_path, and checks that the role is
not a member of the file-reading roles. Do those too. Step 6 of that page,
Check what you built, gives
three statements to confirm it worked. If its CREATE TABLE succeeds, the
role is not read-only, whatever else the script did.
Step 2: install and connect
nlqueries-core needs Python 3.11 or later. The base install includes the PostgreSQL driver and the MCP server:
pip install nlqueries-core
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY
nlqueries health
Register the database with the read-only login:
nlqueries connect postgres --host db.example.com --database mydb \
--user nlqueries --alias dev
Leaving out --password gives a hidden prompt, which keeps the password out of
your shell history. connect tests the connection, writes the connection
details to ~/.nlqueries/connectors.yaml and stores the password in your OS
keychain rather than in that file. It registers the database as
postgres:db.example.com:mydb. The alias saves typing that ID on every later
command.
One thing to know before you run it: the postgres connector requires TLS
by default, and connect tests the connection under that rule, so a server
that offers no TLS cannot be registered. Certificate settings such as
ssl_ca_cert are added by hand to the connector's entry afterwards. See
Transport security.
Step 3: build the knowledge base
The MCP server answers from a knowledge base, not from a live look at the catalogue, and an agent does not exist until its knowledge base has been exported. Three commands build it:
nlqueries extract-schema dev
nlqueries process-history dev --days 30 --annotate
nlqueries export-kb dev
process-history is what makes the knowledge base more than a schema dump.
It reads pg_stat_statements, deduplicates and clusters the queries your team
actually runs, and keeps them as annotated query templates: examples of how
your schema is really used. Without the extension it stops with
✗ Query history unavailable: and the reason. You can
enable history capture
or skip to export-kb, which still writes the full schema.
Why your schema isn't enough explains
what the history adds.
Run export-kb again whenever you re-run process-history. Before involving
Claude, check the agent from the command line:
nlqueries query dev "How many orders shipped last month?"
The MCP server runs the same pipeline, so a problem found here is easier to fix here than through Claude.
Step 4: add the server to Claude Desktop
Over stdio, Claude Desktop starts the MCP server itself and talks to it over
the process's own pipes. This is the configuration from the
CLI reference, for claude_desktop_config.json:
{
"mcpServers": {
"nlqueries": {
"command": "nlqueries",
"args": ["mcp-server", "start"]
}
}
}
nlqueries mcp-server start with no flags is the stdio transport. Two details
decide whether it works the first time:
- Claude Desktop has to find
nlqueries. If you installed into a virtual environment, put the full path to itsnlqueriesexecutable incommand. - Claude Desktop has to pass your LLM key. nlqueries reads settings from
environment variables, or from a
.envfile in the directory it is started in, and only that directory. Claude Desktop starts the server in a directory you do not choose, so give the key to the server through theenvblock Claude Desktop accepts for each server entry.
The connectors file and the knowledge base are read from ~/.nlqueries, and
the password from your OS keychain, so nothing else needs passing.
Restart Claude Desktop and the server's nine tools appear: list_agents,
get_agent_schema, query, submit_feedback, health, invalidate_cache,
list_connectors, get_query_history and get_cache_stats.
Asking questions
Ask Claude something your database can answer: "Which ten customers spent
the most this quarter?" A typical exchange calls list_agents to find the
agent and query to answer. The agent ID is the knowledge base's file name,
so postgres:db.example.com:mydb appears as postgres_db.example.com_mydb.
The server resolves either form to the same connector.
What query returns:
- the answer;
- the generated SQL;
- the rows as a table, saying how many were shown when the result was larger;
- the agent type and latency.
Two optional arguments help when you want to check its work. paraphrase
adds a plain-English reading of the SQL, produced without an LLM, so you can
confirm what it does. explain adds provenance: the route taken, cache hit
or miss, how much knowledge was injected, and the checks that ran.
get_agent_schema shows the tables and columns the agent knows about, which
is the quickest way to see why a question was misunderstood.
The SQL runs read-only. The query tool runs it under the read-only execution
policy, and on PostgreSQL each statement runs in a READ ONLY transaction. The
tool waits 45 seconds and gives the database 40 of them for the statement, so a
slow query is stopped rather than left running after Claude has given up. If
the first question times out, the usual cause is the embedding model loading
inside the server process. Start the daemon once with
nlqueries embed-server start and later calls use it.
Before you share it
Everything above is single-user. Over stdio the caller already has whatever the process has, so there is nothing to authenticate, and the calls are still authorised and audited against the process owner. That changes as soon as the server listens on a network port for other people.
nlqueries mcp-server start --sse binds 127.0.0.1 and will not start
without authentication. Configure either an identity provider
(NLQ_MCP_OIDC_DISCOVERY_URL and NLQ_MCP_OIDC_CLIENT_ID) or a pre-shared
token (NLQ_MCP_STATIC_TOKEN, at least 32 characters), plus
NLQ_MCP_RESOURCE_URL. With authentication on, it also needs a grants file in
NLQ_MCP_GRANTS_FILE. A subject with no grant is denied, and running SQL is a
separate action (query:execute) granted per agent, so a colleague can be
allowed to read one agent's schema and nothing more.
Authenticating the MCP server has the format,
and Letting an LLM query production safely
covers the layers underneath.
Try it
pip install nlqueries-core, then follow the four steps above against a
read-only role. The source and issue tracker are on
GitHub. For how a question becomes
SQL in the first place, see
Text-to-SQL, explained.