Skip to main content

fluid-postgres-mcp

License: MIT Python 3.10+

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 0

Exit 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/db

With 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.sh

Other 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 CLIdocs:

codex mcp add fluid-postgres-mcp \
    --transport stdio \
    --command "uvx fluid-postgres-mcp postgresql://reader:pw@host:5432/db"

Cursor CLIdocs:

agent mcp add fluid-postgres-mcp -- \
    uvx fluid-postgres-mcp postgresql://reader:pw@host:5432/db

Gemini 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 pollution

From 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-mcp

After 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
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

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 readiness

If 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 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:

  • 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_CONNECT

After 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.

Reference scripts

Working examples — copy and adapt:

  • scripts/examples/aws-ssm-ec2-tunnel.py — 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.
  • scripts/examples/aws-ssm-rds-tunnel.py — same lifecycle as above, but uses AWS-StartPortForwardingSessionToRemoteHost so the EC2 acts as a pure SSM forwarder to an RDS endpoint.
  • tests/e2e/fixtures/long_running_passthrough.sh — minimal long-running script: emits DB_URL + READY_TO_CONNECT, then blocks on exec sleep until SIGTERM. Useful as a starting template.
  • tests/e2e/ssm_fixtures.py (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.
  • 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) 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).
  • macOS sleep does not accept infinity. 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_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.
  • Diagnose without shelling onto the box. All script lifecycle 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.

Develop

Work from a clone:

git clone https://github.com/povesma/fluid-postgres-mcp
cd fluid-postgres-mcp
pip install -e ".[dev]"
pytest

Design and fault-injection catalogue: ARCHITECTURE.md · TESTING-METHODOLOGY.md.

Release

Versioning is SemVer; PyPI is the source of truth. The flow that produced v0.1.1:

# 1. Add a `## [X.Y.Z] - YYYY-MM-DD` section to CHANGELOG.md
#    (Added / Changed / Fixed / Removed) and bump version in
#    pyproject.toml. Both go in the release commit:
git add CHANGELOG.md pyproject.toml
git commit -m "chore(release): bump version to X.Y.Z"
git tag -a vX.Y.Z -m "Release X.Y.Z - <one-line summary>"

# 2. Clean and build (build deps via uvx, no global install needed):
rm -rf dist/ build/ *.egg-info
uvx --from build pyproject-build

# 3. Inspect what's actually inside the sdist before publishing.
#    The wheel only ships src/postgres_mcp; the sdist is allowlisted
#    in pyproject.toml [tool.hatch.build.targets.sdist], so anything
#    not in that list must NOT appear here — especially .env, .claude,
#    tasks/, or any other working-tree-only file:
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. Upload to PyPI. Twine's auth contract is TWINE_USERNAME /
#    TWINE_PASSWORD — not PYPI_TOKEN — so source .env to get
#    PYPI_TOKEN into the environment, then pass it via -u/-p so
#    the bridge is explicit. `set -a; source .env; set +a` keeps
#    the value confined to this shell; the token never enters
#    command line history or any tool's stdin/stdout:
set -a; source .env; set +a
.venv/bin/twine upload -u __token__ -p "$PYPI_TOKEN" dist/*

Notes:

  • uvx --from build pyproject-build avoids needing python -m build installed 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-3 tar -tzf listing as a release gate, not a curiosity.
  • After tagging, optionally create a GitHub Release from the tag, using the matching CHANGELOG.md section as the body: gh release create vX.Y.Z -t "vX.Y.Z" -F <(awk '/^## \[X.Y.Z\]/,/^## \[/' CHANGELOG.md | sed '$d').
  • CHANGELOG.md follows Keep a Changelog 1.1.0; the entry is mandatory before the version bump (treat it as a release gate alongside the tar -tzf listing).
  • Changelog audience & size. Entries are for users of the package, not contributors. Each bullet answers "what do I do or expect differently?" — skip internal symbol names; those belong in the commit message. Size guide by SemVer level: patch (0.1.x → 0.1.y): 1–4 bullets, ≤ ~10 lines body; minor (0.x → 0.y): 3–8 bullets grouped Added/Changed/Fixed, ≤ ~25 lines; major: lead with a 1-paragraph migration note, then as long as it needs.

License

MIT — see LICENSE. Forked from crystaldba/postgres-mcp (MIT).

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fluid_postgres_mcp-0.1.3.tar.gz (84.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fluid_postgres_mcp-0.1.3-py3-none-any.whl (88.2 kB view details)

Uploaded Python 3

File details

Details for the file fluid_postgres_mcp-0.1.3.tar.gz.

File metadata

  • Download URL: fluid_postgres_mcp-0.1.3.tar.gz
  • Upload date:
  • Size: 84.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.12

File hashes

Hashes for fluid_postgres_mcp-0.1.3.tar.gz
Algorithm Hash digest
SHA256 757e7ea7bb3b7da1c407bb45fa5bedbd4549903ea3ebc1766bfd4d83a4a58419
MD5 a9a6c763c49faea3291890ad37144e8b
BLAKE2b-256 1fddb1332bc7f26f9238034691cf07d1008612f8581c6c85417083c9f8939f5b

See more details on using hashes here.

File details

Details for the file fluid_postgres_mcp-0.1.3-py3-none-any.whl.

File metadata

File hashes

Hashes for fluid_postgres_mcp-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 cbc2a3c181808e4e362d69fab9331151f5c036b20eb88807bc3d65975339552f
MD5 f735c58037021f0fbad0d89f0cfbdd99
BLAKE2b-256 47bd35e2fb06d6df2adb7218bf57798c6db0c0f8cf071c0c6dc48442b7caba5a

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.3 This release

2 files

0.1.2

2 files

0.1.1

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page