CloudNativePG MCP Server
by waTeim
README.md
# CloudNativePG MCP Server
An MCP server for managing PostgreSQL clusters through the
[CloudNativePG](https://cloudnative-pg.io/) operator.
This version uses the MCP Base scaffold for its server layout, authentication,
container build, Helm chart, prompt registry, and test harness. The previous
manual implementation is retained under `deprecated-v1/` for reference.
## Tool Surface
The server exposes the CloudNativePG tools from the v1 implementation:
- `list_postgres_clusters`
- `get_cluster_status`
- `create_postgres_cluster`
- `scale_postgres_cluster`
- `configure_projected_volumes`
- `resize_postgres_cluster`
- `get_cluster_resize_status`
- `promote_cluster_instance`
- `delete_cluster_instance`
- `delete_postgres_cluster`
- `list_postgres_roles`
- `get_postgres_role_status`
- `create_postgres_role`
- `update_postgres_role`
- `delete_postgres_role`
- `list_postgres_databases`
- `get_postgres_database_status`
- `create_postgres_database`
- `delete_postgres_database`
### Projected volumes
Secrets and ConfigMaps can be projected into the instance pods, which
CloudNativePG mounts under `/projected`. `create_postgres_cluster` accepts
`projected_secrets`, `projected_config_maps`, and `projected_default_mode`;
`configure_projected_volumes` sets them on an existing cluster.
Rather than requiring a full Kubernetes `VolumeProjection`, you name the resource
and, optionally, the directory it maps to. Every key becomes a file named after
the key:
```python
projected_secrets=["db-creds"] # -> /projected/secret/db-creds/{username,password}
projected_config_maps=["app-config"] # -> /projected/config/app-config/{ca.crt,settings.ini}
projected_secrets={"db-creds": "db"} # -> /projected/db/{username,password}
projected_config_maps={"app-config": "conf/app"}
```
The default directory is `secret/<name>` or `config/<name>`, so a Secret and a
ConfigMap sharing a name cannot collide. Directories are relative to `/projected`
— CloudNativePG always mounts the template there and the path is not
configurable, so an absolute directory is rejected.
`projected_default_mode` accepts octal strings like `"0400"` as well as the
decimal integers the Kubernetes API stores. Note that CloudNativePG runs
instances with `fsGroup` set, so Kubernetes may add the group-read bit — a
requested `0400` typically lands as `0440` on disk.
Kubernetes maps projections per key, not per resource, so each Secret and
ConfigMap is read to enumerate its keys. That is a snapshot: **keys added to a
resource afterwards are not projected until you configure the projection again.**
The resource must exist when the projection is configured.
Paths are validated up front. Neither the Cluster CRD nor the CloudNativePG
webhook rejects an absolute path, a `..` element, or two sources claiming the
same file — those are only caught later at Pod admission, which surfaces as
instances that will not start.
`configure_projected_volumes` replaces `.spec.projectedVolumeTemplate` outright
rather than merging, so pass the complete desired set each time; `clear=True`
removes it. Changing projected volumes alters the instance pod spec, so
CloudNativePG performs a rolling restart to apply it. The current projection is
reported by `get_cluster_status` with `detail_level="detailed"`.
Sources these tools do not build (for example `serviceAccountToken` or
`downwardAPI` applied out of band) are shown in the read-back output but are
replaced if you call `configure_projected_volumes`.
### Storage resize
`resize_postgres_cluster` changes `.spec.storage.size`. Growing is applied
directly and CloudNativePG expands the volumes in place. Shrinking cannot be
applied to an existing volume at all, so the tool only *starts* a migration and
returns immediately — replication can take hours, so no tool call blocks on it.
Starting a shrink (requires `confirm_shrink=True`) performs three requests: set
`cnpg.io/validation: disabled`, patch the smaller `.spec.storage.size` together
with an increased `.spec.instances` in a single request so the added instance is
created at the new size, then remove the annotation. Validation is restored even
if the patch fails; `restore_validation_only=True` recovers the annotation if a
run is interrupted.
The rest of the workflow is driven by separate tools, so you decide when each
step happens:
1. `get_cluster_resize_status` — per-instance volume sizes (requested vs.
actual), which instances still hold the previous size, replication health,
and the recommended next action.
2. `promote_cluster_instance` — switchover to the new, smaller instance by
setting `.status.targetPrimary`. Refuses instances the operator does not
report as healthy unless `force=True`.
3. `delete_cluster_instance` — deletes one instance's PVCs and Pod, leaving
`.spec.instances` alone so the operator rebuilds it from the primary at the
current size. Refuses to delete the current primary. Repeat for each instance
still on the old size, one at a time.
4. `scale_postgres_cluster` — return to your original instance count.
Growing needs a storage class with working volume expansion. If
`get_cluster_resize_status` keeps showing `requested / actual` sizes that differ,
the provisioner may advertise `allowVolumeExpansion` without running a resize
controller; the status output points at the PVC events that confirm this.
Roles are managed through CloudNativePG's first-class `DatabaseRole` CRD rather
than the deprecated Cluster `.spec.managed.roles` field. `create_postgres_role`
creates a `DatabaseRole` named `<cluster>-<role>` and, unless
`disable_password` is set, generates a password Secret referenced by the CRD.
Beyond the standard role flags (`login`, `superuser`, `inherit`, `createdb`,
`createrole`, `replication`, `bypassrls`) it exposes the CRD's `in_roles`,
`connection_limit`, `valid_until`, `comment`, `client_certificate`, and
`reclaim_policy`. `update_postgres_role` patches the CRD spec and can rotate the
password Secret; `delete_postgres_role` deletes the CRD, honoring its reclaim
policy, and accepts `drop_role=True` to force the role to be dropped from
PostgreSQL. `get_postgres_role_status` reports the CRD's current spec values and
operator reconciliation status. `list_postgres_roles` lists `DatabaseRole` CRDs
for a cluster and separately reports any legacy `.spec.managed.roles` entries
still present on the Cluster.
`create_postgres_database` supports CloudNativePG Database CRD create-time
locale options, including `encoding`, `locale`, `locale_provider`,
`locale_collate`, `locale_ctype`, `icu_locale`, `icu_rules`,
`builtin_locale`, and `collation_version`.
`get_postgres_database_status` reports the current Database CRD spec values for
those options along with the operator reconciliation status.
`create_postgres_cluster` accepts `container_image` to set the CloudNativePG
`spec.imageName` directly; when omitted it continues to derive the image from
`postgres_version`. It also exposes pod scheduling and storage placement
controls: `storage_class` (`spec.storage.storageClass`), `node_selector`
(`spec.affinity.nodeSelector`), and `tolerations` (`spec.affinity.tolerations`).
Together these enable node-local storage: pin instances with `node_selector`
(e.g. `{"kubernetes.io/hostname": "worker-1"}` for a specific node, or a label
like `{"disktype": "nvme"}` for a pool), select a node-local `storage_class`,
and supply `tolerations` so pods are admitted onto dedicated (tainted) storage
nodes. `image_pull_policy` maps to `spec.imagePullPolicy`.
It also includes MCP Base scaffold admin tools for prompt management:
- `admin_reload_prompts`
- `admin_get_prompt_manifest`
## Layout
- `src/cnpg_mcp_server.py`: production FastMCP HTTP entrypoint
- `src/cnpg_mcp_test_server.py`: no-auth/OIDC test entrypoint
- `src/cnpg_mcp_tools.py`: CloudNativePG tool implementations and registration
- `src/mcp_context.py`: MCP context wrapper with user identity extraction
- `src/auth_*.py`: MCP Base scaffold authentication support
- `chart/`: Helm deployment assets
- `test/`: MCP plugin test harness
- `SCAFFOLD_INVENTORY.md`: MCP Base scaffold artifact hashes
## Development
Create an environment and install dependencies:
```bash
python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt -r test/requirements.txt
```
Run the scaffold registration smoke test:
```bash
python bin/smoke_test.py
```
Run the local no-auth MCP test suite:
```bash
python test/run-local-tests.py
```
Run the CloudNativePG Kubernetes integration tests adapted from
`deprecated-v1/test/plugins`:
```bash
python test/run-local-tests.py --include-integration
# or
make test-integration
```
These tests create, scale, update, and delete real CloudNativePG resources.
Useful optional settings:
- `CNPG_MCP_TEST_NAMESPACE`: namespace for test resources
- `CNPG_MCP_TEST_CLUSTER_PREFIX`: generated cluster name prefix
- `CNPG_MCP_TEST_STORAGE_SIZE`: per-instance storage size, default `1Gi`
- `CNPG_MCP_TEST_CREATE_WAIT_SECONDS`: cluster readiness timeout, default `300`
- `CNPG_MCP_TEST_SCALE_WAIT_SECONDS`: scale readiness timeout, default `300`
## Running Locally
The scaffold entrypoint uses HTTP transport:
```bash
python src/cnpg_mcp_server.py --host 0.0.0.0 --port 4200
```
The test server can be run without authentication:
```bash
python src/cnpg_mcp_test_server.py --host 127.0.0.1 --port 4201 --no-auth
```
## Kubernetes Access
The tools use the Kubernetes Python client. They load configuration in this
order:
1. In-cluster service account configuration
2. Local kubeconfig from `~/.kube/config` or `KUBECONFIG`
Most tools accept an optional `namespace`. When omitted, the current Kubernetes
context namespace is used, falling back to `default`.
For in-cluster Helm deployments, the server uses the deployment service account.
By default the chart grants that service account CNPG and secret permissions
only in the Helm release namespace. To manage CNPG resources in another
namespace, pass the tool's `namespace` argument and grant the service account
access there:
```yaml
rbac:
targetNamespaces:
- application-databases
```
For a shared MCP deployment that must operate in arbitrary namespaces, opt in to
cluster-wide RBAC:
```yaml
rbac:
clusterWide: true
```
Cluster-wide mode grants secret access across namespaces, so prefer explicit
`targetNamespaces` when the target set is known.
## Deployment
The MCP Base scaffold includes Docker and Helm assets:
```bash
make build
make push
make helm-install
```
Use `python bin/configure-make.py` to generate `make.env` for image and
namespace settings before using the deployment targets.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues