fluid-postgres-mcp
Provides tools for executing SQL queries, exploring database schemas, analyzing query performance, and managing PostgreSQL databases. Supports streaming large result sets to CSV, automatic reconnection, and pre-connect scripts for tunnels (e.g., AWS SSM).
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fluid-postgres-mcpshow me the top 5 slowest queries"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
fluid-postgres-mcp
A PostgreSQL MCP server for AI agents. Streams large result sets to CSV, enforces per-query timeouts, auto-reconnects with backoff, and supports long-running tunnel scripts (e.g. AWS SSM port-forwarding) with credential rotation.
Fork of crystaldba/postgres-mcp.
Install
Jump to: Other AI agents · Alternative install methods · Develop
Python 3.10+. Published on PyPI as
fluid-postgres-mcp;
console entry point of the same name.
Verify install
Before wiring the MCP into your agent, confirm the install actually resolved its dependencies:
uvx fluid-postgres-mcp --version # prints "fluid-postgres-mcp X.Y.Z", exit 0
uvx fluid-postgres-mcp --help # prints usage, exit 0Exit 0 from either command means the package downloaded and every
runtime dependency imported successfully. A Python traceback or
non-zero exit means at least one import failed — typically a missing
system library (e.g. libpq on minimal Linux images) or a broken
uvx cache. Fix that before continuing; an agent registration
against a broken install fails silently at first tool call.
With Claude Code (primary)
claude mcp add fluid-postgres-mcp -- \
uvx fluid-postgres-mcp \
postgresql://reader:pw@host:5432/dbWith a long-running tunnel script (see Pre-connect scripts for the protocol the script must speak):
claude mcp add fluid-postgres-mcp -- \
uvx fluid-postgres-mcp \
--pre-connect-script /path/to/your-tunnel.shOther AI agents
Brief one-shot snippets — copy-paste, or read your agent's own MCP
docs for the full story. All entries use uvx fluid-postgres-mcp
so no global install is needed.
Codex CLI — docs:
codex mcp add fluid-postgres-mcp \
--transport stdio \
--command "uvx fluid-postgres-mcp postgresql://reader:pw@host:5432/db"Cursor CLI — docs:
agent mcp add fluid-postgres-mcp -- \
uvx fluid-postgres-mcp postgresql://reader:pw@host:5432/dbGemini CLI — add to ~/.gemini/settings.json
(docs):
{
"mcpServers": {
"fluid-postgres-mcp": {
"command": "uvx",
"args": ["fluid-postgres-mcp", "postgresql://reader:pw@host:5432/db"]
}
}
}opencode — add to opencode.json
(docs):
{
"mcp": {
"fluid-postgres-mcp": {
"type": "local",
"command": ["uvx", "fluid-postgres-mcp", "postgresql://reader:pw@host:5432/db"]
}
}
}Kiro CLI — add to mcp.json
(docs):
{
"mcpServers": {
"fluid-postgres-mcp": {
"command": "uvx",
"args": ["fluid-postgres-mcp", "postgresql://reader:pw@host:5432/db"]
}
}
}Cursor (IDE) — add to ~/.cursor/mcp.json
(docs):
{
"mcpServers": {
"fluid-postgres-mcp": {
"command": "uvx",
"args": ["fluid-postgres-mcp", "postgresql://reader:pw@host:5432/db"]
}
}
}Windsurf — add to ~/.codeium/windsurf/mcp_config.json
(docs):
{
"mcpServers": {
"fluid-postgres-mcp": {
"command": "uvx",
"args": ["fluid-postgres-mcp", "postgresql://reader:pw@host:5432/db"]
}
}
}Zed — add to ~/.config/zed/settings.json under
context_servers (note: not mcpServers)
(docs):
{
"context_servers": {
"fluid-postgres-mcp": {
"command": "uvx",
"args": ["fluid-postgres-mcp", "postgresql://reader:pw@host:5432/db"]
}
}
}Alternative install methods
If you'd rather have a persistent install than resolve through
uvx on every launch:
pipx install fluid-postgres-mcp # isolated, on $PATH
pip install fluid-postgres-mcp # use a virtualenv to avoid global pollutionFrom source (no editable; for users who clone but don't want a working tree):
git clone https://github.com/povesma/fluid-postgres-mcp
pip install ./fluid-postgres-mcpAfter any of these, the agent snippets above can drop uvx and
invoke fluid-postgres-mcp directly.
How to use Fluid Postgres MCP
Tools exposed: execute_sql, status, list_schemas, list_objects,
get_object_details, explain_query, analyze_db_health,
analyze_query_indexes, analyze_workload_indexes, get_top_queries.
execute_sql accepts timeout_ms, output_file, and output_mode
(inline / file / file+inline) for CSV streaming.
Configure
Every flag has a matching env var (PGMCP_*). CLI wins.
Flag | Default | What it does |
| required | PostgreSQL URL |
|
|
|
|
| Backoff bounds (s) |
|
| 0 = unlimited |
| none | Tunnel/setup script (see below) |
|
| Pre-connect-script timeout (s) |
|
| Per-category ring buffer |
|
| Default base for CSV output |
|
|
|
Pre-connect scripts
Two modes, auto-detected from script behaviour. Existing run-and-exit scripts work unchanged.
Run-and-exit: the script runs, exits 0, and the MCP connects. Suitable when something else owns the tunnel.
Long-running: the script owns the tunnel for the lifetime of the MCP. It speaks a line-prefixed stdout protocol:
[MCP] DB_URL postgresql://user:pw@host:port/db # optional, overrides --database-url
[MCP] READY_TO_CONNECT # required, signals readinessIf the script process dies (tunnel broke), the MCP detects it within ~1 second, respawns the script, and reconnects with whatever URL the new instance emits — which is how credential/URL rotation works.
AWS SSM examples
Two vendored Python reference scripts cover the common AWS topologies.
Each is a single drop-in file: copy it, set the env vars, point
--pre-connect-script at it. Both speak the long-running protocol
described above; both supervise their SSM child and exit on its
death so fluid-postgres-mcp respawns them.
Choosing a topology
Topology | Script | When |
EC2-direct | PostgreSQL runs on the EC2 instance itself, or the EC2 hosts a userspace proxy you control. | |
RDS-via-EC2 | PostgreSQL runs on RDS. The EC2 is a pure SSM forwarder — no PG, no proxy on it. Uses |
True bastion-less SSM-to-RDS is not possible: ssm:StartSession
requires an SSM-managed target, and RDS is not one. If you cannot
keep an EC2 in the loop, look at RDS IAM authentication or the EC2
Instance Connect Endpoint (EICE) — both are outside the SSM
port-forwarding model these scripts use.
Passwords in DB_URL are obfuscated in every fluid-postgres-mcp
event message and log line.
Environment variables
The scripts are configured entirely via environment variables,
passed through your agent's MCP registration (e.g. claude mcp add
… -e KEY=VALUE).
Variable | EC2-direct | RDS-via-EC2 | Purpose |
| required | required | SSM target instance |
| required | required | AWS region of the instance |
| — | required | RDS endpoint hostname |
| required | required | PostgreSQL database name |
| required | required | PostgreSQL user |
| required | required | PostgreSQL password |
| optional ( | — | Host PG listens on (EC2-direct only) |
| optional ( | optional ( | PostgreSQL port |
| optional | optional | Role to assume on top of base credentials |
| optional | optional | Profile (overridden by |
Authentication precedence (highest first): --profile CLI flag →
AWS_PROFILE → SDK default credential chain (env vars,
~/.aws/credentials, instance metadata, …). If ASSUME_ROLE_ARN
is set, the resolved base credentials drive an sts:AssumeRole
call and the resulting STS credentials drive every subsequent AWS
call.
Required AWS permissions
The principal that ends up driving the AWS calls (after AssumeRole, if any) needs:
sts:AssumeRole (only if ASSUME_ROLE_ARN set)
sts:GetCallerIdentity (diagnostic)
ec2:DescribeInstances
ec2:StartInstances (only to wake a stopped host)
ssm:DescribeInstanceInformation
ssm:StartSession (see below for resource scope)
ssm:TerminateSession (on the session ARN — clean teardown)The ssm:StartSession resource scope differs by topology:
EC2-direct: target = the EC2 instance ARN; document =
AWS-StartPortForwardingSession.RDS-via-EC2: target = the EC2 instance ARN; document =
AWS-StartPortForwardingSessionToRemoteHost. The EC2's security group must allow egress to RDS:5432; the RDS security group must allow ingress from the EC2 security group.
Stdout protocol
Both scripts emit exactly two lines on stdout (everything else goes to stderr):
[MCP] DB_URL postgresql://<user>:<pw>@127.0.0.1:<local_port>/<db>?...
[MCP] READY_TO_CONNECTAfter these the script stays alive supervising the SSM child. Exit on child death is the signal to fluid-postgres-mcp that the tunnel is gone and the script should be respawned — that is the recovery loop.
EC2-direct
PostgreSQL is reachable on the EC2 itself (running there, or
proxied by the EC2 to a backend it controls). The SSM session
terminates on the EC2 and forwards traffic to whatever
DB_HOST:DB_PORT resolves to from the EC2's perspective
(default localhost:5432).
claude mcp add my-pg \
-e EC2_INSTANCE_ID=i-0123456789abcdef0 \
-e EC2_REGION=eu-central-1 \
-e DB_NAME=mydb -e DB_USERNAME=reader -e DB_PASSWD='s3cr3t' \
-- uvx fluid-postgres-mcp \
--pre-connect-script /path/to/aws-ssm-ec2-tunnel.pyRDS-via-EC2
PostgreSQL runs on RDS. The EC2 is a pure SSM forwarder — no PG
process, no userspace proxy. The SSM session uses
AWS-StartPortForwardingSessionToRemoteHost with host=$RDS_ENDPOINT,
so traffic flows
localhost:<local_port> → EC2 (SSM forwarder) → $RDS_ENDPOINT:5432.
claude mcp add my-pg \
-e EC2_INSTANCE_ID=i-0123456789abcdef0 \
-e EC2_REGION=eu-central-1 \
-e RDS_ENDPOINT=my-db.abcdef.eu-central-1.rds.amazonaws.com \
-e DB_NAME=mydb -e DB_USERNAME=reader -e DB_PASSWD='s3cr3t' \
-- uvx fluid-postgres-mcp \
--pre-connect-script /path/to/aws-ssm-rds-tunnel.pySmoke
After registering, restart your agent and run a real-data query —
not SELECT 1. A constant query proves only that something
answered on the socket; it doesn't prove the right DB was mapped
or that rows flow:
SELECT count(*) FROM <a-known-populated-table>;Expect a non-trivial count you can recognise. Zero / empty / NULL is a failure, not a pass.
Reference scripts
Working examples — copy and adapt:
scripts/examples/aws-ssm-ec2-tunnel.py— production-shaped Python: credential resolution, optionalsts:AssumeRole, EC2 wake, SSM agent readiness wait, port-forward, port-open probe, PG liveness probe, handshake, signal teardown, remote session termination. EC2-direct topology.scripts/examples/aws-ssm-rds-tunnel.py— same lifecycle as above, but usesAWS-StartPortForwardingSessionToRemoteHostso the EC2 acts as a pure SSM forwarder to an RDS endpoint.tests/e2e/fixtures/long_running_passthrough.sh— minimal long-running script: emitsDB_URL+READY_TO_CONNECT, then blocks onexec sleepuntil SIGTERM. Useful as a starting template.tests/e2e/ssm_fixtures.py(create_long_running_tunnel_script) — full SSM port-forwarding variant: spawnsaws ssm start-sessionas a child, fetches the password from Parameter Store, emits the protocol lines, thenwaits on the SSM child so tunnel death exits the script.tests/e2e/ssm_fixtures.py(create_tunnel_script) — run-and-exit SSM variant for the legacy flow (something else owns the tunnel lifecycle).
Authoring notes
Block on the resource that defines liveness. A long-running script must exit when its tunnel/session dies; otherwise the MCP has no signal to reconnect.
wait "$TUNNEL_PID"(foreground child) orexec sleep <large>(when there's no child to wait on) both work; backgroundedsleep & wait $!with atrapis unreliable on macOS (the parent'sproc.wait()does not observe SIGCHLD through it).macOS
sleepdoes not acceptinfinity. Use a large integer (exec sleep 2147483647).Failure surface. Exit-before-READY → mode is run-and-exit and exit code surfaces as success/failure. No
READY_TO_CONNECTwithin--hook-timeout→ script killed, connect fails. Malformed[MCP] DB_URLpayload → warning event recorded, prior override retained, MCP falls back to the configured URL. Unknown[MCP]keywords are warned once per keyword and ignored. Long-running script alive but no[MCP] DB_URLyet → state isWAITING_FOR_URL, the reconnect loop keeps polling, recoverable as soon as the script emits a URL. Run-and-exit script exited without emittingDB_URLand noDATABASE_URI/ positional URL is set →_unrecoverable=True, stateERROR, no further retries.Diagnose without shelling onto the box. All script lifecycle events are exposed via the
statusMCP tool —started/pid,READY_TO_CONNECT,DB_URL(host/db only, password redacted),exited/exit_code. SeeARCHITECTURE.mdfor how the event store is wired andTESTING-METHODOLOGY.mdfor the faults we inject against it.
Develop
Work from a clone:
git clone https://github.com/povesma/fluid-postgres-mcp
cd fluid-postgres-mcp
pip install -e ".[dev]"
pytestDesign and fault-injection catalogue:
ARCHITECTURE.md ·
TESTING-METHODOLOGY.md.
Release
Versioning is SemVer. PyPI is the point of no return — a version uploaded there cannot be edited or reuploaded, only yanked. Everything reversible happens before PyPI publish; GitHub Release stays in draft until PyPI succeeds. A release is not done until git tag, GitHub Release, and PyPI all agree.
The release flow is automated by scripts/release.sh.
The script runs the seven steps below as a single transaction:
pre-PyPI failures roll back (delete local tag + draft Release);
post-PyPI failures surface loudly with the exact recovery command.
scripts/release-check.sh asserts the
three-source agreement (tag / GH Release / PyPI) for any version —
run it any time to catch half-released states.
Author work (the script verifies but does not author):
Add a
## [X.Y.Z] - YYYY-MM-DDsection toCHANGELOG.mdper the CHANGELOG.md authoring rule below.Bump
version = "X.Y.Z"inpyproject.toml.Run
scripts/release.sh— see modes below.
Non-interactive mode (AI agent or scripted, all inputs supplied):
scripts/release.sh --version X.Y.Z \
--release-body-file /path/to/release-body.md \
--yesInteractive mode (human-driven, missing inputs prompt
$EDITOR pre-populated with the relevant authoring rule):
scripts/release.sh # prompts for version + Release body
scripts/release.sh --version 0.1.4 # prompts for Release body onlyThe seven steps the script orchestrates (for reference, and for the rare case the script fails partway and a step must be re-run by hand):
# 1. Tag — version + CHANGELOG already committed by the author
git tag -a vX.Y.Z -m "Release X.Y.Z"
# 2. Clean and build
rm -rf dist/ build/ *.egg-info
uvx --from build pyproject-build
# 3. Inspect sdist + twine check (script rejects .env / .claude /
# tasks leaks via the sdist allowlist)
tar -tzf dist/*.tar.gz | sort
.venv/bin/twine check dist/*
# 4. Push commit and tag
git push
git push origin vX.Y.Z
# 5. Draft GitHub Release. Body is HAND-WRITTEN per the Release
# body rule below — never awk-extracted from CHANGELOG (the
# audiences and styles differ). Draft state lets you proof-read
# before PyPI is committed.
gh release create vX.Y.Z --draft -t "vX.Y.Z" -F /tmp/release-vX.Y.Z.md
# 6. Upload to PyPI — *point of no return*. Twine's auth contract
# is TWINE_USERNAME / TWINE_PASSWORD, not PYPI_TOKEN, so source
# .env to get PYPI_TOKEN into the environment and pass via -u/-p.
# The script redacts pypi-* patterns from any echoed output.
set -a; source .env; set +a
.venv/bin/twine upload -u __token__ -p "$PYPI_TOKEN" dist/*
# 7. Flip the GH Release out of draft and smoke the published
# artefact end-to-end.
gh release edit vX.Y.Z --draft=false
uvx fluid-postgres-mcp --version # expect "fluid-postgres-mcp X.Y.Z"Notes:
uvx --from build pyproject-buildavoids needingpython -m buildinstalled system-wide; the project's hatchling backend is fetched into an isolated env.The sdist contents are controlled by an explicit allowlist in
[tool.hatch.build.targets.sdist].include. Any new top-level file you add to the repo is excluded from the sdist by default — add it to the allowlist if it should ship. Treat the step-3tar -tzflisting as a release gate, not a curiosity.CHANGELOG.mdfollows Keep a Changelog 1.1.0; the entry is mandatory before the version bump (treat it as a release gate alongside thetar -tzflisting).CHANGELOG.md authoring. Audience: someone deciding whether to upgrade, and someone reconstructing history later (downstream packagers, future-you, anyone tracing a regression to a version). Comprehensive but ruthless with wording. Include all user-facing changes, categorised by impact (Breaking, Security, Added, Changed, Fixed, Deprecated). Each item: one sentence stating the change and its user impact. Add a second sentence only when a reader must take action (migration step, version range affected, workaround) — never to explain rationale or implementation. Drop the how and the why; if rationale matters, it lives in the commit message or PR.
GitHub Release body authoring. Audience: someone glancing at the release page or a notification feed, deciding whether this release needs their attention right now. Executive summary. Open with the most consequential change in one sentence; if the release has a coherent theme, name it — if not, don't invent one. Follow with a "Highlights" bullet list of anything a reader of the release page needs to know without opening the CHANGELOG. Breaking, security, deprecations, platform/Python/dependency shifts, and major features will usually qualify; pure bug fixes and internal changes will not. Link to the CHANGELOG for the rest. Hand-written, not awk-extracted.
License
MIT — see LICENSE. Forked from crystaldba/postgres-mcp (MIT).
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/povesma/fluid-postgres-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server