Fluid Postgres Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Fluid Postgres Mcp (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.
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.
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.
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.shBrief 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"]
}
}
}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.
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.
Every flag has a matching env var (PGMCP_*). CLI wins.
| Flag | Default | What it does |
|---|---|---|
database_url (positional) / DATABASE_URI | required | PostgreSQL URL |
--default-timeout | 0 | statement_timeout ms (0 = none) |
--reconnect-initial-delay / --reconnect-max-delay | 1.0 / 60.0 | Backoff bounds (s) |
--reconnect-max-attempts | 0 | 0 = unlimited |
--pre-connect-script | none | Tunnel/setup script (see below) |
--hook-timeout | 30.0 | Pre-connect-script timeout (s) |
--event-buffer-size | 100 | Per-category ring buffer |
--output-dir | . | Default base for CSV output |
--transport | stdio | stdio / sse / streamable-http |
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.
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 | scripts/examples/aws-ssm-ec2-tunnel.py | PostgreSQL runs on the EC2 instance itself, or the EC2 hosts a userspace proxy you control. |
| RDS-via-EC2 | scripts/examples/aws-ssm-rds-tunnel.py | PostgreSQL runs on RDS. The EC2 is a pure SSM forwarder — no PG, no proxy on it. Uses AWS-StartPortForwardingSessionToRemoteHost. |
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 |
|---|---|---|---|
EC2_INSTANCE_ID | required | required | SSM target instance |
EC2_REGION | required | required | AWS region of the instance |
RDS_ENDPOINT | — | required | RDS endpoint hostname |
DB_NAME | required | required | PostgreSQL database name |
DB_USERNAME | required | required | PostgreSQL user |
DB_PASSWD | required | required | PostgreSQL password |
DB_HOST | optional (localhost) | — | Host PG listens on (EC2-direct only) |
DB_PORT | optional (5432) | optional (5432) | PostgreSQL port |
ASSUME_ROLE_ARN | optional | optional | Role to assume on top of base credentials |
AWS_PROFILE | optional | optional | Profile (overridden by --profile flag) |
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:
AWS-StartPortForwardingSession.
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.py#### RDS-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.py#### Smoke
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.
Working examples — copy and adapt:
— production-shaped Python: credential resolution, optional sts:AssumeRole, EC2 wake, SSM agent readiness wait, port-forward, port-open probe, PG liveness probe, handshake, signal teardown, remote session termination. EC2-direct topology.
— same lifecycle as above, but uses AWS-StartPortForwardingSessionToRemoteHost so the EC2 acts as a pure SSM forwarder to an RDS endpoint.
— minimal long-running script: emits DB_URL + READY_TO_CONNECT, then blocks on exec sleep until SIGTERM. Useful as a starting template.
(create_long_running_tunnel_script) — full SSM port-forwarding variant: spawns aws ssm start-session as a child, fetches the password from Parameter Store, emits the protocol lines, then waits on the SSM child so tunnel death exits the script.
(create_tunnel_script) — run-and-exit SSM variant for the legacy flow (something else owns the tunnel lifecycle).
script must exit when its tunnel/session dies; otherwise the MCP has no signal to reconnect. wait "$TUNNEL_PID" (foreground child) or exec sleep <large> (when there's no child to wait on) both work; backgrounded sleep & wait $! with a trap is unreliable on macOS (the parent's proc.wait() does not observe SIGCHLD through it).
(exec sleep 2147483647).
exit code surfaces as success/failure. No READY_TO_CONNECT within --hook-timeout → script killed, connect fails. Malformed [MCP] DB_URL payload → 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_URL yet → state is WAITING_FOR_URL, the reconnect loop keeps polling, recoverable as soon as the script emits a URL. Run-and-exit script exited without emitting DB_URL and no DATABASE_URI / positional URL is set → _unrecoverable=True, state ERROR, no further retries.
events are exposed via the status MCP tool — started/pid, READY_TO_CONNECT, DB_URL (host/db only, password redacted), exited/exit_code. See ARCHITECTURE.md for how the event store is wired and TESTING-METHODOLOGY.md for the faults we inject against it.
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.
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):
## [X.Y.Z] - YYYY-MM-DD section to CHANGELOG.md perthe CHANGELOG.md authoring rule below.
version = "X.Y.Z" in pyproject.toml.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-build avoids needing python -m buildinstalled system-wide; the project's hatchling backend is fetched into an isolated env.
[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-3 tar -tzf listing as a release gate, not a curiosity.
CHANGELOG.md followsKeep a Changelog 1.1.0; the entry is mandatory before the version bump (treat it as a release gate alongside the tar -tzf listing).
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.
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.
MIT — see LICENSE. Forked from crystaldba/postgres-mcp (MIT).
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.