Metadata-Version: 2.4
Name: neon-guard-mcp
Version: 0.1.0
Summary: A local security proxy MCP server for Neon's serverless Postgres API — default-deny, policy-enforced AI branch access.
Project-URL: Homepage, https://github.com/hayian78/neon-guard-mcp
Project-URL: Repository, https://github.com/hayian78/neon-guard-mcp
Project-URL: Issues, https://github.com/hayian78/neon-guard-mcp/issues
Author-email: James <james@fusedair.com.au>
License: MIT
License-File: LICENSE
Keywords: claude,mcp,model-context-protocol,neon,postgres,proxy,security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Database
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Requires-Dist: fastmcp<3,>=2.14
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2
Provides-Extra: dev
Requires-Dist: pytest-asyncio<1,>=0.23; extra == 'dev'
Requires-Dist: pytest<9,>=8; extra == 'dev'
Description-Content-Type: text/markdown

# neon-guard-mcp 🛡️

Neon's official MCP server is amazing, but giving an LLM full admin access to your infrastructure is terrifying. **neon-guard-mcp** acts as a local security proxy, giving your AI agent the exact tools it needs to branch and test code, while completely air-gapping your production data and master keys.

---

## Features

- **Default-deny whitelist** — only a small set of branch-scoped tools are exposed; there is no tool for deleting projects, managing users, modifying production, or accessing connection strings for branches the proxy did not create.
- **Provenance gating** — authorization to retrieve a connection string, delete, or reset a branch requires that the proxy itself created it (tracked in `.neon-guard-state.json`). A matching name prefix alone is not sufficient.
- **Schema-only branches by default** — new branches are created with `init_source: schema-only`, so no production rows are ever copied into an agent-accessible branch.
- **Short-lived branches** — configurable expiry (default 24 h); Neon deletes the branch automatically, invalidating the embedded credential.
- **Project allowlist** — the proxy refuses to act on any project not explicitly listed in your config, even if the API key has broader access.
- **NEON_API_KEY never leaves the proxy process** — the LLM receives only tool results, never the key itself.
- **Configurable tool disabling** — remove individual tools from the schema if your threat model requires it.

---

## Architecture

```
Claude Code (or any MCP client)
        |
        |  stdio (JSON-RPC)
        v
+---------------------------+
|   neon-guard-mcp proxy    |
|  - holds NEON_API_KEY     |
|  - enforces policy        |
|  - tracks provenance      |
|  - rewrites / filters     |
+---------------------------+
        |
        |  HTTPS (Neon REST API v2)
        v
   console.neon.tech
```

The LLM calls tools. The proxy validates every call against config and provenance state, then translates allowed calls into Neon API requests. The master key is only ever present in the proxy process environment.

---

## Why / threat model

### The official Neon MCP server

Neon's official server maps MCP tools 1:1 to Neon REST API operations. This is intentional and useful — for a human developer who understands what they are authorizing. For an autonomous AI agent, it means the LLM can:

- Delete any branch, including `main` / production.
- Read connection strings for any branch, including production.
- Create branches that copy full production data.
- Manage users, roles, and project settings.

One prompt-injection attack, one confused-deputy mistake, or one overly-helpful model decision is enough to destroy production data or exfiltrate it.

### neon-guard-mcp

This proxy takes the opposite position: **default-deny, least privilege, explicit allowlist**. The agent gets only what it needs to create isolated test branches and run migrations/tests against them. Every other API surface simply does not exist as far as the LLM is concerned.

---

## How the trust boundary works

**Name prefix is NOT a security boundary.**

It would be trivial for an attacker (or a confused model) to supply a branch name that happens to match the configured prefix. Authorization is instead gated on *provenance*: the proxy records every branch it creates in `.neon-guard-state.json`. A branch must appear in that record AND pass live server-side checks (not default, not protected, not the enforced parent branch) before the proxy will issue a connection string, delete it, or reset it.

The provenance store uses atomic file writes (write-then-`os.replace`) with mode `0o600` to prevent partial reads and limit filesystem exposure.

---

## Installation

### Recommended: uvx with a pinned version

```bash
uvx neon-guard-mcp==0.1.0
```

Pinning the version is important for security: it prevents a supply-chain update from silently changing proxy behavior.

### pip

```bash
pip install neon-guard-mcp==0.1.0
```

### From source (before the package is published)

Until `neon-guard-mcp` is published to PyPI, install it from a local checkout:

```bash
git clone https://github.com/your-org/neon-guard-mcp.git
cd neon-guard-mcp
uv sync                      # or: pip install -e .
uv run neon-guard-mcp        # runs the server over stdio
```

In your MCP client config, point `command` at `uv` with `args: ["run", "neon-guard-mcp"]` (or `uvx --from /abs/path/to/neon-guard-mcp neon-guard-mcp`) instead of the published-package form below.

---

## Configuration

Copy the example config and edit it:

```bash
cp neon-guard.json.example neon-guard.json
```

> **Note:** `neon-guard.json` and `.neon-guard-state.json` are in `.gitignore` and must never be committed. The example file (`neon-guard.json.example`) is safe to commit — it contains no secrets.

### Configuration schema

| Field | Type | Default | Description |
|---|---|---|---|
| `allowed_projects` | `list[string]` | required | Neon project IDs the proxy is permitted to act on. Any other project ID is rejected. |
| `branch_naming_prefix` | `string` | required | Prefix applied to all branches the proxy creates (e.g. `"ai-dev-"`). Must match `^[a-z0-9][a-z0-9-]*$`. |
| `enforce_parent_branch` | `string` | required | Name of the branch all agent branches must be parented from (e.g. `"main"`). Prevents branching off an already-modified branch. |
| `branch_data_mode` | `"schema-only"` \| `"full"` | `"schema-only"` | Controls Neon's `init_source`. `"schema-only"` copies only DDL — no rows. **Change to `"full"` only if you have reviewed the security implications.** |
| `branch_expiry_hours` | `integer` \| `null` | `24` | Hours after creation before Neon auto-deletes the branch. `null` disables expiry. Expiry also invalidates the embedded credential in any connection string issued for the branch. |
| `allow_branch_deletion` | `bool` | `false` | If `false`, the `delete_test_branch` and `reset_test_branch` tools are not registered at all. |
| `max_ai_branches` | `integer` | `10` | Maximum number of live agent-created branches per project. Prevents runaway branch creation. |
| `connection.default_database` | `string` | required | Database name used when generating connection strings. |
| `connection.default_role` | `string` | required | Postgres role used when generating connection strings. Should be a dedicated least-privilege role (see Recommended Neon Setup). |
| `connection.pooled` | `bool` | `true` | Whether to request a PgBouncer pooled connection URI. |
| `disabled_tools` | `list[string]` | `[]` | Tool names to omit from the MCP schema entirely. Useful for further restricting the agent's surface. |

---

## Available tools

All tools are read-only or scoped to branches the proxy created. No tool can touch production or untracked branches.

| Tool | What it does | Guard |
|---|---|---|
| `list_safe_projects` | Lists Neon projects filtered to `allowed_projects`. | Intersection with server response; unlisted projects are invisible. |
| `list_safe_branches` | Lists branches for an allowed project, annotated with `created_by_guard`. | Project must be in `allowed_projects`. |
| `create_test_branch` | Creates a new branch from `enforce_parent_branch` using the naming prefix. | Project allowed; parent verified server-side; `max_ai_branches` enforced; name uniqueness checked; `branch_data_mode` and `expires_at` applied. |
| `get_branch_connection_string` | Returns the connection URI for a guard-created branch. | Project allowed; branch must be in provenance store; branch must not be default, protected, or the enforced parent. |
| `get_branch_status` | Returns branch state metadata. | Project allowed; branch must exist in the project. |
| `delete_test_branch` | Deletes a guard-created branch. | Only registered if `allow_branch_deletion: true`; provenance + server-side safety checks; branch must have no children. |
| `reset_test_branch` | Restores a guard-created branch to its parent's current state. | Only registered if `allow_branch_deletion: true`; provenance + server-side safety checks. |

---

## Claude Code `.mcp.json` integration

Add this to your project's `.mcp.json` (or `~/.claude/mcp.json` for global use):

```json
{
  "mcpServers": {
    "neon-guard": {
      "command": "uvx",
      "args": ["neon-guard-mcp==0.1.0"],
      "env": {
        "NEON_API_KEY": "${NEON_API_KEY}",
        "NEON_GUARD_CONFIG": "/absolute/path/to/neon-guard.json"
      }
    }
  }
}
```

> **Important:** The `env` block is passed only to the `neon-guard-mcp` subprocess. Claude Code does not forward it to the LLM context. `NEON_API_KEY` never appears in any tool call, tool result, or conversation message.

Note the `"${NEON_API_KEY}"` reference: Claude Code expands `${VAR}` (and `${VAR:-default}`) from your host environment **at server-launch time**, so the literal secret stays out of the config file. Use an absolute path for `NEON_GUARD_CONFIG` to avoid ambiguity about the working directory at startup.

---

## Securing the master key

Claude Code passes `NEON_API_KEY` to the proxy as a **process environment variable** when it launches the server — that is the only way the key reaches the proxy, and it never reaches the model. But Claude Code does **not** encrypt this value anywhere. How you supply it is up to you; here are the options, most secure first.

> ⚠️ **Myth:** "`claude mcp add --env` stores the key encrypted in `~/.claude.json`." **False.** It is stored as **plaintext JSON**. Treat anything you put in a config file or `~/.claude.json` as readable by anyone (or anything) with access to that file.

**1. Secrets manager (recommended).** The literal key never lands in any file — it lives in your vault and is injected into the environment only at launch. With the [1Password CLI](https://developer.1password.com/docs/cli/), reference the key with an `op://` secret reference and let `op run` resolve it:

```json
{
  "mcpServers": {
    "neon-guard": {
      "command": "op",
      "args": ["run", "--", "uvx", "neon-guard-mcp==0.1.0"],
      "env": {
        "NEON_API_KEY": "op://Private/Neon/api-key",
        "NEON_GUARD_CONFIG": "/absolute/path/to/neon-guard.json"
      }
    }
  }
}
```

The same pattern works with `aws-vault exec`, `vault`, `doppler run`, etc. — wrap the launch command, keep the secret in the manager.

**2. Reference a host env var via `${...}` expansion.** Keep the real value in your shell environment (sourced from an uncommitted file or your secrets manager) and reference it — the config stays committable because it holds no secret:

```bash
export NEON_API_KEY="napi_…"   # from ~/.zshrc sourcing an uncommitted ~/.secrets, a vault, etc.
```
```json
"env": { "NEON_API_KEY": "${NEON_API_KEY}" }
```

**3. Local scope via the CLI (fine for a personal machine — but plaintext at rest).** `--scope local` (the default) writes to `~/.claude.json`, which is **not** committed to git, so it's safe from your repo — but the value is plaintext on disk:

```bash
claude mcp add --scope local --env NEON_API_KEY="napi_…" \
  neon-guard -- uvx neon-guard-mcp==0.1.0
```

**Never** hardcode the literal key in a **project-scoped `.mcp.json`** — that file is meant to be committed, so the secret would land in your git history in plaintext. Use `${NEON_API_KEY}` expansion or a secrets-manager wrapper there instead. (`neon-guard.json` and `.neon-guard-state.json` are already git-ignored, but the master key is never written to either of them — it only ever comes from the environment.)

---

## Security model and honest boundaries

### What this protects

- **Master API key** — never in LLM context; lives only in the proxy process environment.
- **Production branch** — no tool can retrieve its connection string, delete it, or reset it (enforced server-side by checking the `default` and `protected` flags and matching against `enforce_parent_branch`).
- **Non-allowed projects** — refused at the policy layer before any API call.
- **Untracked branches** — connection strings and destructive operations are refused for any branch not recorded in `.neon-guard-state.json`.

### What this does NOT fully protect

**Connection strings are live credentials.** When you call `get_branch_connection_string`, the returned URI contains a Postgres password. That URI enters the LLM context (it is the tool result). This is a fundamental constraint of how database connections work — the agent needs a credential to connect.

Mitigations applied by default:

- **Schema-only mode** — even if the credential is exfiltrated, the branch contains no production rows.
- **Short expiry** — the default 24 h expiry causes Neon to delete the branch, which invalidates the embedded password.
- **Dedicated non-owner role** — the connection role (`neon_guard_ai` in the example) should have only the privileges needed for migrations and tests, not superuser or `neondb_owner`.

**Operational posture:** treat any branch that an agent has touched as potentially compromised. Deleting the branch (or allowing it to expire) rotates the credential. Do not reuse agent branches for sensitive data.

**Log redaction is a best-effort backstop, not the primary control.** The proxy never passes the master key or a connection URI to a log, an error message, or stdout, and it scrubs known secret shapes (Neon keys, `Bearer` tokens, `postgresql://` credentials, `password=` keyword forms) from every log record as defense-in-depth. Redaction is pattern-based, so the real guarantee is the "never log a secret" discipline above — do not add log lines that interpolate credentials and rely on the scrubber to catch them.

### Prompt injection risk

If your agent processes untrusted input (web pages, user-provided files, database content from a previous branch), an adversary could embed instructions that attempt to exfiltrate the connection string or manipulate the agent into calling `reset_test_branch` on the wrong branch. The schema-only default and provenance gating significantly reduce the blast radius, but they do not eliminate the attack surface entirely. Consider whether your agent's task requires access to `get_branch_connection_string` at all; if not, add it to `disabled_tools`.

---

## Recommended Neon setup

Create a dedicated database role with limited privileges rather than using `neondb_owner`:

```sql
-- Run as neondb_owner on your Neon project
CREATE ROLE neon_guard_ai WITH LOGIN PASSWORD 'choose-a-strong-password';

-- Grant only what your migrations/tests need
GRANT CONNECT ON DATABASE neondb TO neon_guard_ai;
GRANT USAGE ON SCHEMA public TO neon_guard_ai;
GRANT CREATE ON SCHEMA public TO neon_guard_ai;  -- needed for migrations
-- Do NOT grant SUPERUSER, CREATEROLE, or CREATEDB
```

Set `connection.default_role` to `"neon_guard_ai"` in your config. This ensures that even if a connection string is leaked, the role cannot escalate privileges, create new roles, or access other databases.

---

## License

MIT — see [LICENSE](LICENSE).
