codegiveness.postgresql-sharp-mcp
0.6.0
See the version list below for details.
dotnet tool install --global codegiveness.postgresql-sharp-mcp --version 0.6.0
dotnet new tool-manifest
dotnet tool install --local codegiveness.postgresql-sharp-mcp --version 0.6.0
#tool dotnet:?package=codegiveness.postgresql-sharp-mcp&version=0.6.0
nuke :add-package codegiveness.postgresql-sharp-mcp --version 0.6.0
postgresql-sharp-mcp — PostgreSQL tools for your AI assistant
<img src="docs/assets/social-preview.png" width="640" alt="postgresql-sharp-mcp: an MCP server in C#/.NET that gives your AI assistant PostgreSQL tools to discover databases, inspect schemas, run SQL, read query plans, analyze indexes and check health; read-only SQL by default; installs with npx or as a .NET tool on Linux, macOS and Windows.">
Let your AI assistant connect to PostgreSQL, list the databases you can access, inspect tables and run SQL, with query plans, index analysis and database health checks built in.
Who it is for: developers, DBAs and data engineers who have a PostgreSQL login and use an MCP-compatible AI assistant or coding agent that starts local (stdio) servers. This app is a Model Context Protocol server written in C# with Npgsql. Your MCP client starts the app when needed; it does not create a PostgreSQL database for you.
SQL calls are read-only by default, but the server's default access mode permits explicitly requested writes. Use a least-privileged database login, never a superuser. Keep an existing explicit restricted setting; access boundaries explain how to refuse writes entirely.
Fastest start: Node.js 22+ and npm are enough, because npm installs the .NET runtime with the server. This downloads and runs version 0.6.0:
npx -y --allow-scripts=@codegiveness/postgresql-sharp-mcp @codegiveness/postgresql-sharp-mcp@0.6.0 --version
It should print postgresql-sharp-mcp 0.6.0.0. Then register it with your MCP client and enter your connection string: the quick start has three steps. Prefer a .NET tool or a standalone archive for Linux, macOS or Windows? See step 1. To build and run a container image from the Dockerfile, see CONTRIBUTING.md.
Upgrading from 0.5.0 or earlier? 0.6.0 verifies TLS by default: a remote connection string without
SSL Modenow needs a certificate the server can verify, or an explicitSSL Mode. Run--validatebefore reconnecting, and read the upgrade and rollback note.
What you can ask once it is connected:
| You ask your assistant | Tools it calls |
|---|---|
| "Which databases can I access on this server?" | list_databases |
"Show the columns and indexes of public.orders in app_db." |
list_objects, get_object_details |
| "How many orders were placed yesterday?" | execute_sql, read-only by default |
"Why is this query slow? Would an index on customer help?" |
explain_query, optionally with hypothetical HypoPG indexes |
| "Are any indexes unused or duplicated? Is vacuum keeping up?" | analyze_indexes, analyze_db_health |
| "Which statements take the most total time?" | get_top_queries, with pg_stat_statements |
Every result is a row- and byte-bounded page that the assistant can continue with next_offset (result format). The tool reference lists all nine tools and their options.
Quick start
For one PostgreSQL server, you need one connection string—not a targets file or an entry for every database. You also need access to an existing database and an installed MCP client. The walkthrough below uses OMP. It stores only the executable path and nonsensitive options in OMP's configuration; you enter credentials privately each session.
Already installed and registered? Skip to step 2. Switching from a targets file? Open Advanced setup, troubleshooting and legacy profiles below first. Keep your existing file.
1. Install or run
Choose npx for on-demand execution or a .NET tool for a persistent command. Starting with 0.3.3, npm bundles the .NET runtime: npx requires only Node.js 22+ and npm, not an installed .NET runtime or SDK. The .NET tool still requires the .NET 10 runtime, and installing it requires the .NET 10 SDK.
This guide targets 0.6.0. Registry commands require the corresponding published version; if it is unavailable, build and install the local npm artifact. npm 0.3.2 still requires .NET; installing it globally does not remove that prerequisite.
npx — run without a global install:
npx -y --allow-scripts=@codegiveness/postgresql-sharp-mcp @codegiveness/postgresql-sharp-mcp@0.6.0 --version
The package-specific --allow-scripts approves installation on npm 12. Do not use --ignore-scripts for automatic setup. npm downloads only the platform package matching your OS/CPU (about 35–39 MB; the main package is under 1 MB). Its self-contained C# installer then installs the server and runtime. Starting with 0.5.0, npm no longer downloads all five platform runtimes. The installer downloads nothing itself. The installed server command is native; it needs neither Node.js nor dotnet to run.
.NET tool — install once, then run by name:
dotnet tool install --global codegiveness.postgresql-sharp-mcp --version 0.6.0
postgresql-sharp-mcp --version
If the command is not found, add the .NET tools directory to your PATH ($HOME/.dotnet/tools on Linux/macOS; %USERPROFILE%\.dotnet\tools on Windows), or use its absolute executable path. Do not reinstall to fix PATH.
The command should report 0.6.0 (or assembly version 0.6.0.0). Continue with registration below, then prepare your connection environment in step 2. Package versions are immutable; a published release archive is an alternative if your registry is unavailable.
<details> <summary>Standalone archives — no .NET, Node.js or npm installation needed</summary>
Download one archive and SHA256SUMS from release 0.6.0. These archives include the .NET runtime: you do not need the .NET SDK, npm, Node.js or Docker for this installation.
| Your computer | Archive |
|---|---|
| Linux, Intel/AMD 64-bit | postgresql-sharp-mcp-linux-x64.tar.gz |
| Linux, ARM64 | postgresql-sharp-mcp-linux-arm64.tar.gz |
| Mac, Apple Silicon (M-series) | postgresql-sharp-mcp-osx-arm64.tar.gz |
| Mac, Intel | postgresql-sharp-mcp-osx-x64.tar.gz |
| Windows, Intel/AMD 64-bit | postgresql-sharp-mcp-win-x64.zip |
Extract into postgresql-mcp/app inside your home folder, keeping all the extracted files together. Open the instructions for your OS below to check the download, extract it and check the version. Commands assume you saved the archive in Downloads.
<details> <summary>Linux: verify and extract</summary>
Compare the hash printed by the first command with your archive's entry in the downloaded SHA256SUMS. Stop if they differ; extract only after they match. For ARM64, replace both archive filenames with postgresql-sharp-mcp-linux-arm64.tar.gz.
sha256sum "$HOME/Downloads/postgresql-sharp-mcp-linux-x64.tar.gz"
mkdir -p "$HOME/postgresql-mcp/app"
tar -xzf "$HOME/Downloads/postgresql-sharp-mcp-linux-x64.tar.gz" -C "$HOME/postgresql-mcp/app"
"$HOME/postgresql-mcp/app/PostgreSqlMcp" --version
</details>
<details> <summary>macOS: verify and extract</summary>
Compare the hash printed by the first command with your archive's entry in the downloaded SHA256SUMS. Stop if they differ; extract only after they match. For an Intel Mac, replace both archive filenames with postgresql-sharp-mcp-osx-x64.tar.gz.
shasum -a 256 "$HOME/Downloads/postgresql-sharp-mcp-osx-arm64.tar.gz"
mkdir -p "$HOME/postgresql-mcp/app"
tar -xzf "$HOME/Downloads/postgresql-sharp-mcp-osx-arm64.tar.gz" -C "$HOME/postgresql-mcp/app"
"$HOME/postgresql-mcp/app/PostgreSqlMcp" --version
</details>
<details> <summary>Windows: verify and extract in PowerShell</summary>
Compare the hash printed by the first command with your archive's entry in the downloaded SHA256SUMS (case does not matter). Stop if they differ; extract only after they match.
Get-FileHash "$HOME\Downloads\postgresql-sharp-mcp-win-x64.zip" -Algorithm SHA256
New-Item -ItemType Directory -Force "$HOME\postgresql-mcp\app" | Out-Null
Expand-Archive -Path "$HOME\Downloads\postgresql-sharp-mcp-win-x64.zip" -DestinationPath "$HOME\postgresql-mcp\app"
& "$HOME\postgresql-mcp\app\PostgreSqlMcp.exe" --version
</details>
The executable should report 0.6.0. If you already have an older installation, read upgrade notes before reusing its configuration.
</details>
Register with OMP once. Edit or create ~/.omp/agent/mcp.json on Linux/macOS, or %USERPROFILE%\.omp\agent\mcp.json on Windows (for a named OMP profile, use that profile's MCP configuration). Installing this app does not register it automatically.
For npx, use this JSON for a new file. If the file already has other servers, add only the "postgresql" entry inside its existing mcpServers object; preserve every other entry and separate adjacent entries with a comma.
{
"mcpServers": {
"postgresql": {
"type": "stdio",
"command": "npx",
"args": ["-y", "--allow-scripts=@codegiveness/postgresql-sharp-mcp", "@codegiveness/postgresql-sharp-mcp@0.6.0"]
}
}
}
For a .NET tool, replace command with "postgresql-sharp-mcp" and remove args; the client must inherit the tools directory on PATH. Alternatively, use the absolute tool path (/home/YOUR_USER/.dotnet/tools/postgresql-sharp-mcp on Linux, /Users/YOUR_USER/.dotnet/tools/postgresql-sharp-mcp on macOS, or "C:\\Users\\YOUR_USER\\.dotnet\\tools\\postgresql-sharp-mcp.exe" in Windows JSON).
For a standalone archive, remove args and use the absolute executable path, not the folder or archive:
- Linux:
/home/YOUR_USER/postgresql-mcp/app/PostgreSqlMcp - macOS:
/Users/YOUR_USER/postgresql-mcp/app/PostgreSqlMcp - Windows JSON:
"C:\\Users\\YOUR_USER\\postgresql-mcp\\app\\PostgreSqlMcp.exe"
Replace YOUR_USER with your home-folder name, or adjust the whole home prefix if yours differs. echo "$HOME" on Linux/macOS or $HOME in PowerShell shows your home path. JSON does not expand ~, $HOME or %USERPROFILE%. Do not add credentials to this file. Preserve any intentional "POSTGRES_ACCESS_MODE": "restricted" setting when updating an existing entry.
2. Prepare the connection environment
Enter one complete .NET/Npgsql key=value connection string at the hidden prompt below. Your provider's .NET/Npgsql string is accepted; a raw postgres:// or postgresql:// URL is not. Replace all <...> placeholders, including angle brackets, with your database details. This example is a format guide, not a command:
Host=<YOUR_HOST>;Port=5432;Database=<YOUR_DATABASE>;Username=<YOUR_DATABASE_USER>;Password=<YOUR_PASSWORD>;SSL Mode=VerifyFull
Database is an accessible starting database, such as your existing database or postgres; the same login can discover other accessible databases on that server. A remote server's certificate is verified by default (SSL Mode=VerifyFull, see TLS). For special characters, provider CA certificates or a local server without TLS, open connection-string guidance.
Linux/macOS — paste into your current Bash or Zsh terminal. No shell change or startup-file edit is needed. Then type or paste the connection string at the hidden prompt and press Enter:
unset POSTGRES_TARGETS POSTGRES_TARGETS_FILE POSTGRES_CONNECTION_STRING
printf 'PostgreSQL connection string (hidden): ' >&2
IFS= read -r -s POSTGRES_CONNECTION_STRING && export POSTGRES_CONNECTION_STRING
printf '\n' >&2
Windows — use the built-in Windows PowerShell (5.1 or newer) in an interactive terminal:
Remove-Item Env:POSTGRES_TARGETS, Env:POSTGRES_TARGETS_FILE, Env:POSTGRES_CONNECTION_STRING -ErrorAction SilentlyContinue
$connection = Read-Host 'PostgreSQL connection string (hidden)' -AsSecureString
try {
$env:POSTGRES_CONNECTION_STRING = [System.Net.NetworkCredential]::new('', $connection).Password
} finally {
$connection.Dispose()
Remove-Variable connection
}
Keep this terminal open. Enter the secret at the prompt, never in a command, MCP JSON, chat or shell startup file. Do not print the variable to check it. Hidden entry prevents display/history exposure; the exported environment is still plaintext process memory, inherited by child processes—not encrypted storage.
3. Validate, start OMP and ask for databases
Fully quit any existing OMP process first. In the same terminal from step 2, run the command for your OS. It checks connectivity and starts a new OMP process only if validation succeeds:
Use the command for your installation method. If you use a standalone archive, replace the command with its absolute executable path followed by --validate.
npx, Linux/macOS:
npx -y --allow-scripts=@codegiveness/postgresql-sharp-mcp @codegiveness/postgresql-sharp-mcp@0.6.0 --validate && omp
.NET tool, Linux/macOS:
postgresql-sharp-mcp --validate && omp
Windows PowerShell (.NET tool):
postgresql-sharp-mcp --validate
if ($LASTEXITCODE -eq 0) {
omp
} else {
throw 'Connection validation failed; repeat step 2 or check your database details.'
}
For npx in PowerShell, replace the first line with npx -y --allow-scripts=@codegiveness/postgresql-sharp-mcp @codegiveness/postgresql-sharp-mcp@0.6.0 --validate; keep the $LASTEXITCODE check and launch block.
Validation is read-only and exits; its successful stderr output includes your database name, so keep it private. Empty input is rejected before OMP starts. If it fails, fix your connection details and repeat step 2; troubleshooting covers common failures.
In OMP, ask: “Use the PostgreSQL MCP server to list the databases I can access, then run SELECT 1 AS connection_ok in the starting database using alias primary.” You are ready when it lists real database names and the query returns 1.
The client must inherit this terminal's environment. A desktop/Start-menu launch, an already-running client, or /mcp reload cannot pick up a newly entered secret. For a later session or changed password, fully quit OMP, repeat step 2, and launch it again from that prepared terminal. You do not repeat registration or edit a file for new databases. Other MCP clients need their own configuration schema and a launcher that starts a new process with the inherited environment; GUI clients that cannot do this need the advanced file alternative.
When finished, fully quit OMP/server and clear the terminal's secret with unset POSTGRES_CONNECTION_STRING (Bash/Zsh) or Remove-Item Env:POSTGRES_CONNECTION_STRING -ErrorAction SilentlyContinue (PowerShell). This does not erase copies in children that are still running.
Optional help: Click a section title below to open it.
<details> <summary>Advanced setup, troubleshooting and legacy profiles</summary>
Connection-string quoting and TLS
Use the database host and port supplied by your provider, an accessible bootstrap database, and a PostgreSQL login role with CONNECT to the intended databases and only the object privileges it needs. The database username is not necessarily your computer's username.
At the hidden prompt, quoting has one layer: Npgsql, not JSON or shell syntax. Quote a value containing a semicolon, for example the synthetic fragment Password="sample;value". Inside a double-quoted value, double a literal double quote (Password="sample""value"). Backslashes are entered literally; do not add JSON's \" or double backslashes for JSON. Prefer a provider-generated .NET/Npgsql string over constructing one by hand.
If your provider supplies only a PostgreSQL URL, map its host, port, database, username and password to the example's fields, decode URL-escaped values, and preserve required TLS/authentication options. The URL itself cannot be entered unchanged.
TLS is verified by default. If a connection string does not set SSL Mode, the server uses SSL Mode=VerifyFull for every non-loopback host: the certificate chain and host name are verified before any credential is sent, and the connection never falls back to plaintext. A server whose certificate is signed by a public CA works unchanged; for a provider or private CA add Root Certificate=<absolute-path-to-provider-CA-file> (see Npgsql's TLS requirements). The host name you connect to must appear in the certificate. For localhost, 127.0.0.0/8, ::1 and Unix-domain sockets, an omitted SSL Mode means SSL Mode=Disable: nothing leaves the computer, and the server never uses TLS without verifying it (a Root Certificate on a loopback profile selects VerifyFull instead). A multi-host string counts as local only if every host is. Because only an omitted SSL Mode is rewritten, a PostgreSQL that requires TLS on loopback (hostssl rules only, or a forwarded remote port) needs an explicit SSL Mode=Require or VerifyFull.
An explicit SSL Mode (any value) is your decision and is used unchanged. A remote profile with Disable, Allow, Prefer or Require still works, but prints a [warning] at startup and in --validate because the server is then not verified. Do not use these to work around a certificate error; fix the trust material instead. --validate also warns when the login is a superuser or holds server-file, signal or checkpoint roles (details). For PostgreSQL on this same computer without TLS, use Host=127.0.0.1 and omit SSL Mode.
Upgrading an existing installation
Automatic discovery and the unrestricted default require 0.3.1 or newer: the contract was developed under source version 0.3.0, which was never tagged or released, so 0.3.1 is the first release that contains it. npm runtime bundling requires 0.3.3 or newer. 0.4.0 added the database lock: an existing POSTGRES_DATABASES configuration refuses to start unless PostgreSQL grants match the list (see the hardening guide). 0.5.0 extended that check: it also refuses a login that can execute server-file functions such as pg_read_binary_file in a locked database, so revoke such grants before upgrading a locked configuration. This guide targets 0.6.0, which verifies TLS by default (next paragraph). Older 0.2.0 executables use configured database aliases and a restricted default. Changing configuration does not upgrade an old executable.
0.6.0 breaking change: TLS is verified by default. A profile that does not set SSL Mode used to get Npgsql's default Prefer, which encrypts without verifying the server and falls back to plaintext. From 0.6.0 it gets SSL Mode=VerifyFull for a non-loopback host and SSL Mode=Disable for loopback/Unix-socket hosts. Remote servers with a publicly trusted certificate need no change. A remote server with a private CA fails with tls_verification_failed: add Root Certificate=<CA file>. A remote server without TLS fails with tls_unavailable, including a local PostgreSQL reached as host.docker.internal. Profiles that already set SSL Mode are unchanged. Before upgrading, run the new version's --validate against your configuration. To roll back for one profile while staying on 0.6.0, set SSL Mode=Prefer explicitly in it (or Require, or Disable for a server without TLS); the startup warning then names the profile. To roll back the whole installation, pin 0.5.0 (dotnet tool update --global codegiveness.postgresql-sharp-mcp --version 0.5.0, or @0.5.0 in the npx args); 0.5.0 reads the same configuration.
After publication, update an existing global .NET tool with dotnet tool update --global codegiveness.postgresql-sharp-mcp --version 0.6.0. For npx, update the version in your client's args to @codegiveness/postgresql-sharp-mcp@0.6.0; retain package-specific script approval. For archives, fully quit the client/server and replace the extracted files together. Recheck --version before reconnecting.
Breaking behavior: a targets-file entry is now a host/login connection profile, not a database allowlist. The same credentials can select other physical databases on that PostgreSQL server. Omitted access mode means unrestricted; list_databases returns live databases rather than configured aliases. Preserve an existing explicit POSTGRES_ACCESS_MODE=restricted and review PostgreSQL grants before upgrading.
Rolling back the 0.4.0 lock startup refusal: if an existing POSTGRES_DATABASES/--databases configuration is refused after upgrading, you do not have to give up the upgrade permanently. Read the refusal findings on stderr, then either (a) pin the previous release (0.3.3: dotnet tool update --global codegiveness.postgresql-sharp-mcp --version 0.3.3, or @0.3.3 in the npx args), which enforces the list inside the server without the PostgreSQL grant check, and remove --require-database-lock from args because 0.3.3 does not recognize it and exits with "Unknown option" (a POSTGRES_REQUIRE_DATABASE_LOCK environment variable is ignored by 0.3.3, so it is harmless there but no longer enforces anything), or (b) temporarily remove POSTGRES_DATABASES/--databases and any POSTGRES_REQUIRE_DATABASE_LOCK/--require-database-lock setting so the server starts on 0.4.0. Both are stopgaps: 0.3.3 accepts a lock only with a connection string (not with targets JSON/file), and removing the lock leaves the login's full PostgreSQL privileges reachable, so keep that login least-privileged and restore the lock soon. Fix the findings with the hardening guide, restore the lock, and re-run --validate before reconnecting the client.
To migrate an existing MCP entry to the environment workflow, remove POSTGRES_TARGETS, POSTGRES_TARGETS_FILE and literal POSTGRES_CONNECTION_STRING from its PostgreSQL env, plus --targets-file/--connection-string arguments. Preserve other nonsensitive settings, especially restricted access mode. Step 2 clears conflicting variables only in the current shell. Combining a connection string with targets JSON/file fails closed. Protected files can remain on disk; do not delete or rename them merely to switch workflows.
Optional reusable Bash/Zsh prompt in your rc file
This is optional; the quick start needs no rc changes. The same function works in Bash and Zsh and validates before a chained launch. Save only the function definition, never a credential or an automatic invocation. Preserve your existing startup configuration.
postgres_mcp_env() {
unset POSTGRES_TARGETS POSTGRES_TARGETS_FILE POSTGRES_CONNECTION_STRING
printf 'PostgreSQL connection string (hidden): ' >&2
IFS= read -r -s POSTGRES_CONNECTION_STRING && export POSTGRES_CONNECTION_STRING
printf '\n' >&2
"$HOME/postgresql-mcp/app/PostgreSqlMcp" --validate
}
Adjust the executable path if yours differs. EOF does not export partially entered input; validation rejects missing/empty credentials. Call postgres_mcp_env && omp only after fully quitting the existing client and completing registration. Do not call the function in a subprocess and expect it to change the parent environment. It does not modify credential files.
- Bash: an interactive non-login shell reads
~/.bashrc. Login Bash reads the first existing readable file among~/.bash_profile,~/.bash_login,~/.profile, not all three; it does not automatically read.bashrc. If needed, let your existing login file conditionally source.bashrcfor interactive Bash. Do not replace your files or put shell-specific commands into a shared.profile. See Bash startup files. - Zsh: use
~/.zshrc, or$ZDOTDIR/.zshrcif customized, for interactive shells..zprofileis login-only;.zshenvalso runs in noninteractive shells and is not a place for secret prompts. See Zsh startup files.
After reviewing and saving the chosen file, source only your current shell's rc file: source "$HOME/.bashrc" in Bash or source "${ZDOTDIR:-$HOME}/.zshrc" in Zsh. Sourcing executes all commands in the file. No shell-specific prompt option is used.
Optional protected targets file
Choose this instead of the connection-string environment for multiple hosts/logins or GUI/automation launches where session inheritance is impractical. It is not required for one-server discovery. Existing targets.json, targets-0.2.0.json or other protected filenames remain supported; use the actual path without a rename or per-database entries.
Before switching, fully quit the client/server. Clear POSTGRES_CONNECTION_STRING from the launching environment (unset POSTGRES_CONNECTION_STRING in Bash/Zsh; Remove-Item Env:POSTGRES_CONNECTION_STRING -ErrorAction SilentlyContinue in PowerShell), and remove its client env entry and --connection-string argument. Clear stale POSTGRES_TARGETS too: that JSON source takes precedence over a file. Combining a base string with targets JSON/file is rejected.
For a new Linux/macOS file, create private permissions before adding credentials:
mkdir -p "$HOME/postgresql-mcp"
chmod 700 "$HOME/postgresql-mcp"
touch "$HOME/postgresql-mcp/targets.json"
chmod 600 "$HOME/postgresql-mcp/targets.json"
On Windows, restrict the file/folder to your account and administrators with Windows permissions. In Notepad's Save As dialog select All files to avoid targets.json.txt. Avoid shared/publicly synced folders. OS permissions do not encrypt contents; consider backup/sync exposure and privileged access.
Populate it with a plain-text editor, using the connection-string/TLS guidance above:
{
"primary": "Host=<YOUR_HOST>;Port=5432;Database=<YOUR_DATABASE>;Username=<YOUR_DATABASE_USER>;Password=<YOUR_PASSWORD>;SSL Mode=VerifyFull"
}
A file has two escaping layers: Npgsql, then JSON. The synthetic Npgsql fragment Password="sample;value" becomes Password=\"sample;value\" in a JSON string. Double a literal quote inside an Npgsql quoted value, then escape each quote for JSON; double JSON backslashes too. A JSON-aware editor/serializer helps. No trailing commas are allowed. Never commit the file or paste it into chat/public reports.
For separate hosts/logins, add case-sensitive profile keys such as reporting, each with its own bootstrap string—not an entry per database. A profile is not an allowlist; add a database lock (POSTGRES_DATABASES) to confine every profile to named databases. Optional target selects a profile explicitly; see selection rules.
In the MCP entry, add only "POSTGRES_TARGETS_FILE" in env with the actual absolute file path, alongside nonsensitive options. Linux: /home/YOUR_USER/postgresql-mcp/targets.json; macOS: /Users/YOUR_USER/postgresql-mcp/targets.json; Windows JSON: "C:\\Users\\YOUR_USER\\postgresql-mcp\\targets.json". This path points to the credentials file, not MCP JSON. Do not also add a connection string.
Validate from the shell where conflicting variables were cleared:
"$HOME/postgresql-mcp/app/PostgreSqlMcp" --targets-file "$HOME/postgresql-mcp/targets.json" --validate
& "$HOME\postgresql-mcp\app\PostgreSqlMcp.exe" --targets-file "$HOME\postgresql-mcp\targets.json" --validate
Use your actual paths and keep validation stderr private. Reload the saved MCP configuration (OMP: /mcp reload) or fully restart the client. After changing credentials/profiles, restart the server; new databases/grant changes require no edits or restart. The file route avoids relying on a session secret, but conflicting inherited connection strings must still be removed before launching.
If setup does not work
| What you see | What to check |
|---|---|
| Executable not found / spawn error | Use an absolute executable path including .exe on Windows, not an archive/folder; keep extracted files together. |
| Wrong architecture / cannot execute binary | Match the archive to your OS and CPU. |
| npm reports blocked install scripts / native command cannot execute | Use the full npx command above with package-specific --allow-scripts; if a previous install skipped scripts, use a fresh npm cache for the corrected command. |
npm install fails with postgresql-sharp-mcp-npm-install: not found (or "is not recognized") |
npm did not install the platform package for this machine. Remove --omit=optional/--no-optional and any omit=optional npm setting. If a project package-lock.json was created on another OS, npm can omit this platform's package (npm/cli#4828); delete node_modules and the lockfile, then reinstall. Supported platforms are Windows x64, glibc Linux x64/arm64 and macOS x64/arm64; on Alpine/musl, Windows on Arm or other platforms, use the .NET tool or a container. |
| npx or an MCP client's npx entry fails and prints nothing (on Linux, exit code 127) | npx hides install-script output. Rerun the same command in a terminal with --foreground-scripts added after npx -y to see it, then follow the row above if it shows postgresql-sharp-mcp-npm-install: not found (or "is not recognized"), or the installer's own postgresql-sharp-mcp: ... message otherwise. |
| .NET runtime not found | npm 0.3.2 and earlier require .NET; upgrade to 0.3.3 or newer after publication or use a local fixed artifact. The .NET tool still requires .NET 10 and DOTNET_ROOT for custom runtime locations. npm 0.3.3 and newer and standalone archives bundle the runtime. |
| Invalid configuration / missing credentials | Repeat the hidden prompt, clear stale credential sources from the shell/client entry, and start a new client from that shell. Raw PostgreSQL URLs are not accepted. For files, check the path, JSON escaping and accidental .txt extension. |
| Connection refused, timeout or authentication error | Run --validate; check host, port, database, login/password, network/VPN and PostgreSQL access rules. Installing this app does not grant access. |
tls_verification_failed (certificate validation error) |
The server certificate is not signed by a trusted CA or does not name the host you connect to. Use the provider's host name and add Root Certificate=<absolute-path-to-CA-file>. Do not switch to Prefer/Require to hide it; SSL Mode=Require is the explicit, warned opt-out when verification is impossible. Trust Server Certificate=true does not bypass VerifyFull. |
tls_unavailable |
The server does not offer TLS and the profile requires it (the default for non-loopback hosts). Enable TLS on the server, or set SSL Mode=Disable explicitly for a trusted network. |
| No PostgreSQL tools in OMP | Check MCP configuration location/schema and executable path, then fully quit and launch OMP from the prepared shell. Reload alone cannot supply a missing inherited secret. |
| Invalid target / database missing or denied | target must match a configured profile; database may be a real name. Check spelling, CONNECT and object privileges. Selection never falls back. |
Server waits silently without --validate |
Normal: stdio mode waits for MCP messages. Let the client launch it; use --validate for a terminal check. |
Normal stdout is reserved for MCP JSON-RPC; diagnostics use stderr. Keep credentials, SQL and results out of public reports. OMP timeout is in milliseconds (30000 = 30 seconds); POSTGRES_QUERY_TIMEOUT is in seconds (10 = 10 seconds).
Other installation methods
Choose one installation method from step 1 and follow the same environment/client setup. If registry installation is unavailable, use the self-contained archive, a verified local NuGet package, or a source build. Downloadable npm/NuGet release package files alone do not establish registry publication.
After publication, get a global npm command with npm install --global --allow-scripts=@codegiveness/postgresql-sharp-mcp @codegiveness/postgresql-sharp-mcp@0.6.0, then postgresql-sharp-mcp --version. No .NET installation is required. Use "command": "postgresql-sharp-mcp" with no args in MCP JSON, or its absolute executable path if the client does not inherit npm's global bin directory on PATH.
For a source build, use dotnet as MCP command and the absolute path to PostgreSqlMcp.dll as the first args item. Keep nonsensitive settings and launch the client from the prepared shell. Self-contained builds still need native OS libraries; Debian/Ubuntu GSS/Kerberos may require libgssapi-krb5-2.
Docker (build from source). No container image is published to a registry, so build one from a clone of this repository (the Dockerfile is at its root):
docker build -t postgresql-sharp-mcp .
MCP client entry. It carries no credential: -e POSTGRES_CONNECTION_STRING without a value makes Docker copy the variable from the environment of the process that starts docker, so launch the client from the shell prepared in step 2. -i keeps stdin open for MCP stdio; --rm removes the stopped container.
With "command": "docker", the MCP entry's env block reaches only the docker CLI, not the server inside the container. The container receives only what you pass explicitly: forward every setting (POSTGRES_ACCESS_MODE, POSTGRES_DATABASES, POSTGRES_REQUIRE_DATABASE_LOCK, POSTGRES_QUERY_TIMEOUT, ...) with its own -e NAME before the image name, or pass it as a CLI option after the image name (for example --access-mode restricted). The image sets no defaults, so a setting that is not forwarded is silently absent: the server runs unrestricted with no database lock and no error. The example keeps restricted mode and a required database lock:
{
"mcpServers": {
"postgresql": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "POSTGRES_CONNECTION_STRING",
"-e", "POSTGRES_ACCESS_MODE",
"-e", "POSTGRES_DATABASES",
"-e", "POSTGRES_REQUIRE_DATABASE_LOCK",
"postgresql-sharp-mcp"
],
"env": {
"POSTGRES_ACCESS_MODE": "restricted",
"POSTGRES_DATABASES": "[\"<app_db>\"]",
"POSTGRES_REQUIRE_DATABASE_LOCK": "true"
}
}
}
}
Omit the POSTGRES_DATABASES/POSTGRES_REQUIRE_DATABASE_LOCK lines (both -e pairs and env entries) if you do not use a database lock; the lock needs the PostgreSQL grants described in the hardening guide.
Host=127.0.0.1orlocalhostinside the container is the container itself, not your computer. For PostgreSQL on the Docker host use a host name the container can resolve, such ashost.docker.internal(Docker Desktop; on Linux Engine add"--add-host=host.docker.internal:host-gateway"toargsbefore the image name), or"--network", "host"on Linux.host.docker.internalis not loopback, so TLS is verified by default: a host PostgreSQL without TLS needsSSL Mode=Disablein the connection string. For a remote server use its real host name and keepSSL Mode=VerifyFull.- The container environment is plaintext to Docker inspection and privileged operators; it is not a secret store. Restart the container after credential changes.
- For a protected targets file instead, clear
POSTGRES_CONNECTION_STRINGand add a read-only bind mount plus-e POSTGRES_TARGETS_FILE=/run/postgres.targets.json(full command in CONTRIBUTING). The container runs as a non-root user, so achmod 600file owned by your account is unreadable inside it and startup fails with the generic "Invalid targets configuration" error. Grant that user read access through a suitable group/UID or a secret mount; do not make the file world-readable.
</details>
<details> <summary>Tool and database-selection reference</summary>
Tools
Except list_databases, every tool requires database. Every tool accepts optional target to select a configured connection profile. Query and metadata page defaults are min(100, POSTGRES_MAX_ROWS); top-query pages default to min(10, POSTGRES_MAX_ROWS).
| Tool | Capability | Options |
|---|---|---|
list_databases |
Live accessible physical databases, selected profile, access mode, whether a database lock applies, and limits | target, limit, offset |
list_schemas |
Schemas with USAGE privilege | literal prefix, include_system, page |
list_objects |
Tables, views, materialized views, sequences, functions, procedures and extensions | schema, type, literal search, include_system, page |
get_object_details |
One object's metadata section | schema, name, section: columns/constraints/indexes/triggers/definition/parameters; type, identity_arguments, page |
execute_sql |
One SQL statement; bounded results or explicitly enabled writes | sql, parameters (values for $1..$n), read_only, page |
explain_query |
Estimated/actual JSON plan and compact major-node summary | sql, parameters, format: summary/json, analyze, optional HypoPG indexes |
analyze_indexes |
Index size, usage, validity, constraints and structural duplicate evidence | schema, table, page |
get_top_queries |
Current-database pg_stat_statements workload |
order_by: total_time/mean_time/calls/rows/reads, page |
analyze_db_health |
Summary or focused PostgreSQL health evidence | section: summary/vacuum/index/constraints/sequences/replication/blocking; schema where applicable, page |
parameters binds up to 256 JSON scalars (string, number, boolean or null) to $1..$n in order. Values are sent separately from the SQL text, never interpolated, so quotes and statement-like text in a value stay data. Each value is sent as PostgreSQL's untyped unknown, so the server infers its type from context exactly as for a quoted literal (WHERE created_at > $1 with "2026-01-20"). Cast where context is ambiguous ($1::int, $1::jsonb); objects and arrays are refused, so pass JSON documents as strings and cast. Too few values for the highest $n returns SQLSTATE 08P01; extra values, or a skipped placeholder number such as $2 without $1, return 42P18 (the server cannot infer a type for an unreferenced parameter, so cast the placeholders you use and send no more values than the statement references). The server is stateless: to fetch the next page, resend the same sql and parameters with next_offset, which are re-bound on each call. Parameters do not change read-only enforcement.
Routine overloads require the exact identity_arguments from list_objects, including parameter names; an empty string selects zero arguments. Use type to disambiguate relation/routine name collisions. Discovery filters by role privileges; missing or hidden objects return an error. Table definitions are structural fragments, not a round-trip DDL export.
What clients and agents are told follows the enforced configuration. In unrestricted mode execute_sql is annotated readOnlyHint: false, destructiveHint: true and its description explains the read_only=false write opt-in; in restricted mode it is annotated read-only and its description says writes are refused. All other tools are annotated read-only in both modes. The MCP initialize result carries server instructions naming the access mode, whether a database lock applies, and the result/time limits; under a lock, list_databases and execute_sql descriptions say only listed databases can be selected. Annotations are hints about this server's own enforcement, not a guarantee about side effects of privileged PostgreSQL functions; see access boundaries.
Each call independently resolves its physical database and leases a connection with that database in its connection string. Concurrent calls do not share a current database or issue USE; a failed selection never falls back to another database.
Focused inspection:
{"name":"list_objects","arguments":{"database":"primary","schema":"public","type":"table","search":"order","limit":20}}
{"name":"get_object_details","arguments":{"database":"primary","schema":"public","name":"orders","section":"indexes","limit":10}}
Database selection and live discovery
A profile is a case-sensitive name mapped to a bootstrap connection string: POSTGRES_CONNECTION_STRING alone creates primary, while optional targets JSON/file defines named profiles. The default profile is primary when configured, otherwise the ordinal-first profile name.
- Without
target: an exact configured alias indatabaseselects that alias's bootstrap database for compatibility. Any other value is a physical database name on the default profile. - With
target: that profile supplies the host, authentication and TLS settings;databaseis always a physical database name. Use this form to select a database whose name happens to match an alias. - Only the connection string's
Databasechanges. Tool calls cannot supply credentials, change the host, or rewrite the protected file. An unknown profile returnsinvalid_target; a nonexistent physical database returns PostgreSQL SQLSTATE3D000, and insufficient privilege returns42501. Connectivity failures remain errors, not fallback requests. - With a database lock (
POSTGRES_DATABASES), every resolved physical database—fromdatabase, an alias's bootstrap database ortarget—must be on the list, otherwise the call returnsinvalid_targetwithout connecting. See Database lock.
For example, discover and select databases on the primary profile:
{"name":"list_databases","arguments":{"target":"primary","limit":20,"offset":0}}
{"name":"execute_sql","arguments":{"target":"primary","database":"tenant_b","sql":"SELECT current_database() AS selected_database","limit":1}}
list_databases queries live pg_catalog.pg_database on the selected profile's bootstrap database (under a database lock, the first locked database when the bootstrap database is not locked). It excludes templates, databases with connections disabled and databases for which the current role lacks CONNECT; a database lock further filters the page. Names are ordered with PostgreSQL COLLATE "C", and is_current marks the database used for that listing. New databases and grant/revoke changes appear on subsequent calls without restarting; listing does not guarantee network/authentication or object access for a later connection.
The response is { "target": "...", "databases": { ... }, "access_mode": "...", "database_lock": true|false, "limits": { ... } }. databases is the same bounded query-page shape described below, with columns named name and is_current, positional rows such as [["postgres",true],["tenant_b",false]], and offset, next_offset, truncated, truncation_reason and clipped_cells. Follow databases.next_offset using the same target and limit; pages are live queries, not a shared snapshot. The obsolete targets alias-list payload is no longer returned. Catalog failures are reported rather than silently replaced with configured aliases.
Database lock
To protect databases that must never be touched, run one server instance per PostgreSQL server and set POSTGRES_DATABASES (or --databases) to a JSON array of the exact databases that instance may use, for example ["<app_db>"]. It works with either a connection string or a targets JSON/file and does not change their formats. The server rejects every other database on every path, and at startup and in --validate it verifies that PostgreSQL itself confines the login: it refuses to start if the role can connect to any other database, is or can become a superuser, has CREATEDB/CREATEROLE, owns an out-of-lock database, holds server-file roles or explicit EXECUTE on server-file functions such as pg_read_binary_file, or has dblink/postgres_fdw/foreign servers in a locked database. Set POSTGRES_REQUIRE_DATABASE_LOCK=true (or --require-database-lock) to make a missing lock a startup error. Default PostgreSQL grants give PUBLIC CONNECT on postgres and template1, so most roles need hardening first: follow the database lock hardening guide, which also includes a multi-instance harness example.
Supported PostgreSQL versions
PostgreSQL 13 or newer is supported. The integration verifier runs the MCP scenarios against official Docker images of majors 13 through 18 in CI. PostgreSQL 13 reached community end of life in November 2025, so the minimum can rise in a later release. Older servers are untested and unsupported; some catalog queries need 13 (regcollation) and there is no compatibility layer below it.
PostgreSQL 19 is tested as a pre-release. As of 2026-10-11 the PostgreSQL project had published 19 Beta 4 but no release candidate or final release, and its supported-versions list ends at 18. CI runs the same integration scenarios against the official postgres:19beta4-bookworm image, an informational Integration (PostgreSQL 19) job that is not a required check. Beta behavior can still change before the final release, so 19 is not a support commitment until the fixture moves to the released image and passes.
| Area | Requirement and behavior |
|---|---|
| All tools | PostgreSQL 13+ |
get_top_queries |
pg_stat_statements extension 1.8+ (the PostgreSQL 13 layout). The toplevel column is filled with extension 1.9+ (PostgreSQL 14+ default); with 1.8 the column is still returned, always null, and the notes say so. A cluster upgraded with pg_upgrade keeps its old extension version until ALTER EXTENSION pg_stat_statements UPDATE runs in each database; below 1.8 the tool returns extension_outdated with that instruction instead of a generic column error. |
get_object_details |
Column collations use regcollation (13+). PostgreSQL 18 virtual generated columns are shown as such. |
analyze_indexes |
NULLS NOT DISTINCT (PostgreSQL 15+) is compared when the server has it. |
explain_query with indexes |
HypoPG package for the server's major version (no preload needed). |
Plans and optional extensions
No extensions are installed automatically. An authorized administrator must configure extensions in the databases where they are needed:
pg_stat_statements: install the server extension package, preload it at server start (shared_preload_librariesin the server configuration, or the equivalent parameter-group/console setting of a managed service, without removing existing entries), restart PostgreSQL as that setting requires, and runCREATE EXTENSION pg_stat_statementsin each selected database. After a major-version upgrade, also runALTER EXTENSION pg_stat_statements UPDATEthere. It is needed only for workload statistics.- HypoPG: install the extension package matching the PostgreSQL server major version, then run
CREATE EXTENSION hypopgin the selected database. HypoPG itself does not require preload or a restart. It is needed only for hypotheticalindexesinexplain_query.
Extension schemas are discovered and quoted rather than assumed to be public. Missing or unready extensions produce explicit errors. Check availability and installation before changing a database:
SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name IN ('hypopg', 'pg_stat_statements');
Example what-if call:
{"name":"explain_query","arguments":{"database":"primary","sql":"SELECT * FROM public.orders WHERE customer=42","indexes":["CREATE INDEX ON public.orders(customer)"]}}
Hypothetical candidates are passed to HypoPG, not executed as permanent DDL. The tool compares baseline and combined-candidate planner costs in one session, cleans hypothetical indexes after success or failure, and clears the pool if cleanup fails. At most 16 candidates are accepted; analyze=true cannot be combined with hypothetical indexes. Independent candidate commands are batched into one database round trip.
Planner costs are estimates, not measured speedups or automatic recommendations. The summary ranks at most eight nodes by inclusive subtree cost, reports omitted nodes and does not sum overlapping costs. Health checks likewise report catalog/statistics evidence rather than universal severity thresholds. Sequence estimates depend on increment direction, caching and cycling. Replication reports database-local logical slots, not server-wide physical replication. Blocking PIDs are textual PostgreSQL arrays; another database's blocking query text is omitted.
Workload text is filtered by current database OID even for roles with server-wide monitoring privileges. Tracked statements may include transaction/session setup when pg_stat_statements.track_utility is enabled. High call counts alone do not identify expensive application queries; choose a relevant time, I/O or row ranking.
</details>
<details> <summary>Result format, bounded results and pagination</summary>
Bounded results and pagination
Successful results provide a JSON object in MCP structuredContent and a compact JSON text compatibility block. Consume one representation rather than inserting both into model context. Column names/types appear once and rows use positional arrays, preserving duplicate column names:
{
"database":"primary",
"columns":[{"name":"id","type":"integer"},{"name":"customer","type":"integer"}],
"rows":[[1,42],[2,17]],
"offset":0,
"next_offset":2,
"truncated":true,
"truncation_reason":"row_limit",
"clipped_cells":[]
}
Metadata sections wrap the page in page; health/index/workload tools use result; list_databases uses databases. Optional null envelope fields are omitted; SQL NULL remains null. DML without RETURNING includes rows_affected when known; DDL returns an empty rowset. bytea values are base64 strings with their PostgreSQL type retained.
- Repeat the same read-only operation and filters with
next_offset. SELECT/WITH/VALUES/TABLE reads use PostgreSQLLIMIT limit+1 OFFSET offset; other read-only statement types use streaming row bounds. - Pages re-execute SQL and do not share a snapshot. Use a stable unique
ORDER BY; use keyset SQL for changing datasets or deep pagination. Offsets are capped at 1,000,000. truncation_reasonisrow_limitorbyte_limit. The next offset advances by returned rows, so a byte-truncated row is not skipped.clipped_cellsidentifies zero-based returned-row/column coordinates. Text is bounded by UTF-16 characters without splitting surrogate pairs; binary uses a corresponding base64 budget. Remaining byte space may shorten cells further. Cell clipping is distinct from row truncation.- Retrieve long values deliberately with SQL projections such as
substring(large_text FROM 257 FOR 256)orsubstring(binary_column FROM 193 FOR 192). PostgreSQL text substring positions count Unicode characters, not JSON escape bytes. - Results allow at most 128 columns. Arrays and unbounded provider-specific composite mappings require slicing or casting in SQL. Calendar-month intervals and numbers outside .NET's scalar range return
unsupported_result_valuewith projection guidance. - A row, plan or envelope that cannot fit produces an error requesting a narrower projection/section, smaller limit or operator-approved larger budget. Complete planner JSON must fit even in summary mode; it is not silently cut into invalid JSON.
- Writes are never paginated or replayed. A truncated
RETURNINGresponse has no next offset; the full statement still commits once. Inspect committed data separately. No automatic query retries are performed.
POSTGRES_MAX_RESULT_BYTES bounds each payload representation, not the complete JSON-RPC envelope. Structured JSON plus compatibility text increases wire size. Actual context/token usage depends on the client and model; no cross-server efficiency claim is made.
A local restricted-mode tools/list run of this version returned nine tools in 10,516 UTF-8 bytes for the compact tool array (0.4.0: 6,780 bytes); every parameter now carries a short description with its allowed values or default. The initialize instructions add about 500 bytes once per session. These are discovery bytes, not model-token counts, query timings or a comparison with an “average MCP.” Client/model tokenization and result selection determine context cost.
Operation errors set MCP isError=true and include target, error code and PostgreSQL SQLSTATE when available, with a fixed SQLSTATE-specific summary. For SQL submitted to execute_sql or explain_query, position gives PostgreSQL's 1-based character position within that statement. Syntax and access-rule errors that PostgreSQL positions inside the submitted statement (SQLSTATE class 42, plus 42501 privilege errors; not raised inside a routine or internal query) add PostgreSQL's server_message and server_hint, for example syntax error at or near "WHERE" or column "x" does not exist; integrity errors (class 23) add the violated constraint name. Other PostgreSQL messages, hints and details are withheld because they may contain data values (details). Protocol/SDK argument-validation errors use the SDK envelope.
</details>
<details> <summary>Security, access and resource boundaries</summary>
Access and resource boundaries
Unrestricted mode is the default when access mode is omitted. Each read-only operation owns a transaction with SET TRANSACTION READ ONLY. Client SQL is lexically limited to one statement and cannot issue transaction/session control. The statement-boundary lexer handles comments and PostgreSQL quoting; it is not a SQL authorization AST.
For authorized writes, explicitly pass read_only=false to execute_sql; the PostgreSQL role must also have the necessary privileges. The server commits once on success and rolls back on failure. Default calls remain read-only in unrestricted mode. To refuse writes, opt into POSTGRES_ACCESS_MODE=restricted or --access-mode restricted; existing explicit restricted settings remain effective. explain_query and metadata/operations tools remain read-only; tool annotations advertise potentially destructive SQL execution in unrestricted mode.
Transaction/session controls, COPY, DO, CALL, PREPARE and VACUUM are unsupported in SQL tools; use an administrative client. Routine metadata inspection is supported and functions can be queried with SELECT.
Never use a superuser role. Read-only transactions do not sandbox PostgreSQL functions, SECURITY DEFINER routines, foreign servers/dblink, external side effects, session settings or privileged monitoring. Profiles constrain host/authentication/TLS, not which physical databases those credentials can access. PostgreSQL must enforce CONNECT, schema/table/column privileges and RLS; review PUBLIC CONNECT grants. Use separate credentials for tenants requiring separate authorization. An optional explicit database allowlist narrows selection but is not database authorization. Treat SQL results as untrusted data, not agent instructions. The server exposes stdio, not an authenticated remote transport.
Session credentials are plaintext environment state readable by sufficiently privileged local processes and inherited by children. They are not automatically persisted, but supervisors, launchers, containers or deliberate persistence can store them. Protected files are persistent plaintext with OS permissions, not encryption. Neither route is inherently safer in every deployment; see credential storage tradeoffs. Hidden PowerShell input uses a SecureString only while reading; conversion to the environment is plaintext.
Resource management:
- Lazy, thread-safe
NpgsqlDataSourceper effective normalized connection configuration, including physical database, credentials and TLS; equivalent profiles share a pool. Live listing opens the bootstrap connection. - 1–32 configured profiles, with profiles × configured pool size at most 256. The runtime cache is separately bounded to
floor(256 / POSTGRES_POOL_SIZE)database pools, so selecting new databases does not accumulate unbounded pools. Default pool maximum is 8 physical connections, minimum 0. - Idle least-recently-used pool entries are evicted and disposed when capacity is needed; in-flight entries are never evicted. If every entry is active, new selections wait within the original operation deadline. Failed database selections do not accumulate pool entries.
- Idle connections above the minimum are pruned after 60 seconds with a 10-second interval; physical lifetime is 1,800 seconds. Data sources are disposed on shutdown; operations dispose connections, readers and transactions.
- Default concurrency is 16 database operations per process. Excess calls queue with cancellation; the whole-operation deadline includes queue/pool waits. Statement, lock and command timeouts are also applied.
- Pool limits, reset-on-close, enlistment, multiplexing, application name and logging-safety settings are server-owned. Input enabling
No Reset On Closeor multiplexing is rejected. Credentials and TLS remain operator-controlled. - Metadata and database catalogs are queried on demand, without a result cache or per-operation preflight. This avoids stale privilege/schema results and cross-database cache leakage.
A pre-discovery Linux/.NET 10.0.12 stress review exercised 650 successful text/binary queries, 30 PostgreSQL errors, 30 JSON plans, three deadlines and post-timeout recovery. Post-full-GC managed heap was 5.41–5.44 MB across the last three snapshots; file descriptors stayed at 159, and shutdown left no MCP database sessions. The unchanged baseline also stabilized. These historical finite measurements do not verify the new dynamic pool cache or prove that every workload is leak-free: GC/array-pool retention and working set are different measurements, and result limits are not a process-memory ceiling.
An extended run added 3,200 successful queries, 50 errors, 50 JSON plans and one deadline. Its last three post-full-GC snapshots were 5.49, 5.50 and 5.50 MB, with 160 file descriptors throughout those snapshots. This longer sample supports stabilization after warm-up, not an absolute no-leak guarantee.
For one synthetic 32-row response over 2,000 warmed serialization calls, allocated bytes per response fell from 36,304 to 27,736 (23.6%) after replacing the temporary JSON document/clone round trip with SerializeToElement. This is an allocation measurement, not a throughput or model-token claim.
Each tool call sends the transaction setup (SET TRANSACTION READ ONLY/READ WRITE and timeouts) in the same network batch as its first statement, so a single-statement call costs two round trips (setup + statement, then ROLLBACK/COMMIT) instead of three. Through a local proxy adding 25 ms to each server response, the median execute_sql latency fell from 106.8 ms to 80.8 ms (60 calls each); savings scale with the network round-trip time and are not a throughput claim.
SDK/provider payload logging is disabled even at debug/trace levels; host diagnostics stay on stderr. Query results, metadata and workload text may still contain sensitive data readable by the role. This is not general-purpose data redaction. See SECURITY.md.
</details>
<details> <summary>Configuration reference</summary>
Configuration reference
For one server, use inherited POSTGRES_CONNECTION_STRING; it creates the primary seed and enables live discovery without targets JSON. Choose a base connection string or targets JSON/file; either may add a database lock. Combining a connection string with either targets source is rejected, not silently prioritized. Within the targets-only route, POSTGRES_TARGETS takes precedence over the file. CLI flags override corresponding environment variables, except that POSTGRES_CONNECTION_STRING overrides --connection-string; avoid CLI secrets because process arguments/history can expose them. Profile/credential changes require a server restart; environment secret changes also require fully restarting the client from the newly prepared shell. Live database/grant changes do not.
| Environment | Default / bounds |
|---|---|
POSTGRES_TARGETS |
JSON profile-name-to-bootstrap-connection-string object; each string needs an explicit Host and Database; aliases are nonblank, ≤128 characters, without control characters; wins over POSTGRES_TARGETS_FILE |
POSTGRES_TARGETS_FILE |
Protected JSON profile file; --targets-file supported and overrides it |
POSTGRES_CONNECTION_STRING |
Base Npgsql string; Database defaults to postgres if omitted in this connection-string mode only (targets profiles must include Database); wins over --connection-string |
SSL Mode (inside each connection string) |
Omitted: VerifyFull for non-loopback hosts, Disable for loopback and Unix sockets. Explicit values, including Prefer and Require, are used unchanged; Root Certificate=<CA file> supplies the CA for VerifyCA/VerifyFull |
POSTGRES_DATABASES |
Optional database lock: JSON array of 1+ unique physical database names (each 1..63 UTF-8 bytes, no NUL; no 32-name limit; with a connection string the names are target aliases of one login profile and do not count toward the profile limit), with a connection string or targets JSON/file; startup verifies PostgreSQL grants enforce it; --databases supported |
POSTGRES_REQUIRE_DATABASE_LOCK |
false; true makes a missing POSTGRES_DATABASES a startup error; --require-database-lock supported |
POSTGRES_ACCESS_MODE |
unrestricted when omitted; opt into restricted to refuse writes; --access-mode supported |
POSTGRES_QUERY_TIMEOUT |
30 seconds; 1–600; --query-timeout supported |
POSTGRES_MAX_ROWS |
1000; 1–5000 |
POSTGRES_MAX_RESULT_BYTES |
65536; 4096–1048576 |
POSTGRES_MAX_CELL_CHARS |
4096; 1–16384 |
POSTGRES_POOL_SIZE |
8; 1–32; connection profiles × size ≤256 (a connection string is one profile, whatever the lock size); runtime database-pool cache ≤floor(256 / size) |
POSTGRES_MAX_CONCURRENT_CALLS |
16; 1–64 |
POSTGRES_LOG_LEVEL |
warning; trace/debug/information/warning/error/critical/none; --log-level supported |
A base connection string alone creates the primary profile and supports live discovery. With a POSTGRES_DATABASES database lock, each locked name becomes a seed alias and replaces any Database in the base string; with targets JSON/file, profiles keep their aliases and the lock applies to all of them. In both modes catalog results are filtered to the locked names, selection of any other physical database is rejected, and startup refuses to run unless PostgreSQL grants enforce the same lock (details). The lock must be maintained when databases are added. The optional protected targets file supports independent credentials/multiple profiles and launch environments where session propagation is impractical; neither method requires putting secrets in process arguments. Environment variables are plaintext inherited state, while files are persistent plaintext protected by OS permissions—choose according to the deployment's exposure and lifecycle, not a blanket safety claim.
The .NET tool and framework-dependent source builds require the .NET 10 runtime; npm 0.3.3 and newer and standalone archives include it. Self-contained executables still require native OS libraries. For example, Debian/Ubuntu GSS/Kerberos support uses libgssapi-krb5-2; install the platform's appropriate library if that authentication is needed. Password fallback does not verify Kerberos support. The container includes this dependency. See Npgsql security and encryption.
</details>
Project badges
The complete badge set, grouped by area. Development and licensing explains what the badges do and do not certify.
| Area | Badges |
|---|---|
| Build & tests | |
| Security | |
| Supply chain | |
| Distribution | |
| Platforms | |
| Stack | |
| Features | |
| Project |
Development and licensing
See CONTRIBUTING.md for source builds, container usage, artifact installation, verification and contribution guidelines. The integration fixture defaults to PostgreSQL 17 and CI also runs 13–16, 18 and the 19 beta; this is a verification target, not a guarantee for every platform, hosting provider or MCP client.
The server, npm installer, packaging, verification and release automation are C#/.NET. Workflow badges and Scorecard report checks and practices, not certifications. Feature, platform and practice badges link to the documentation or configuration behind them; they describe implemented capabilities, not guarantees. Best Practices is the saved owner self-assessment and may be in progress. See Security posture for supply-chain evidence, SBOMs and limits, and SECURITY.md for reporting.
Project documents: GOVERNANCE.md (decisions, roles and what happens if the maintainer is unavailable), roadmap, architecture, security assurance case, code of conduct and accessibility statement.
Technical references: Npgsql data sources, pool parameters, sequential access, C# MCP SDK, PostgreSQL EXPLAIN, read-only transactions, pg_stat_statements and HypoPG.
Project source is MIT, copyright codegiveness. Dependency licenses remain separate: Npgsql uses the PostgreSQL license; the MCP SDK uses Apache-2.0. See THIRD-PARTY-NOTICES.md and bundled LICENSES/ for versions, attribution and exact dependency terms.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.