Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `plenum mcp` connection binding at server registration: `--project-path`, `--name`, and `--dsn-env` decouple resolution from the launcher's cwd ([REF-297](/REF/issues/REF-297))
- MCP `connect` save mode (`save`: `"local"` | `"global"`, `project_path`, `name`) that persists connections by credential reference; `password_env` and `keychain_service`/`keychain_account` accepted on `connect`, `introspect`, and `query` ([REF-298](/REF/issues/REF-298))
- `plenum connect` wizard offers plaintext, env var, shell command, and OS keychain credential sources ([REF-299](/REF/issues/REF-299))
- `mcp_stanza` in `plenum connect --save` output: a ready-to-paste `mcpServers` block for the saved connection ([REF-299](/REF/issues/REF-299))
- JSON Schema `schemas/connect_save_success.json` for the `connect --save` envelope, covered by schema drift ([REF-304](/REF/issues/REF-304))
- `--diff-against` flag for `plenum introspect` — read-only schema diff against a prior snapshot ([REF-281](/REF/issues/REF-281))
- Live-DB test harness: Docker Compose fixtures, vendor seed SQL per engine, gated test suites, and `scripts/test-live.sh` orchestration ([REF-275](/REF/issues/REF-275))
- MySQL 8.0 and 8.4 live test matrix: connect, introspect, query, safety, and envelope coverage ([REF-276](/REF/issues/REF-276))
Expand All @@ -27,11 +32,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- DuckDB: compile against `duckdb` 1.10506.0, where `Value` is `#[non_exhaustive]`; `UHUGEINT` converts to a string, `GEOMETRY` (WKB) to Base64, and an unknown value type is a structured `QUERY_FAILED` error instead of a guess
- `plenum connect --project-path X --save local` reported success but persisted nothing when run from another directory; it now writes `X/.plenum/config.json` ([REF-303](/REF/issues/REF-303))
- PostgreSQL: correct NULL detection, composite foreign-key introspection, and view definitions ([REF-277](/REF/issues/REF-277))
- MySQL: route text-protocol statements correctly; classify `EXPLAIN`; surface timeout as a first-class error ([REF-258](/REF/issues/REF-258))
- Build: vendor OpenSSL for hermetic release builds ([REF-258](/REF/issues/REF-258))
- Capability: reject `WITH`-CTE DML bypasses across all engines ([REF-41](/REF/issues/REF-41))

### Security

- MCP `connect` rejects inline plaintext `password` with `CAPABILITY_VIOLATION` ([REF-298](/REF/issues/REF-298))
- All MCP tools reject `password_command` with `CAPABILITY_VIOLATION`: it executes a shell command, so accepting it from tool arguments gave agents arbitrary command execution. The CLI flag and wizard still offer it.

### Changed

- SQLite: tighten read-only `PRAGMA` allowlist ([REF-44](/REF/issues/REF-44))
Expand Down
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,20 @@ Plenum is exposed via a local MCP server.

The Plenum CLI remains the execution boundary.

**Connection binding (`plenum mcp` flags):**
- `--project-path <path>` — resolve config for that project, not the server's cwd
- `--name <connection>` — select a named connection within that project
- `--dsn-env <VAR>` — read the DSN from exactly that env var; never from ambient vars such as `DATABASE_URL`
- `--dsn-env` conflicts with `--project-path` and `--name`
- Connection fields in a tool call override the binding
- No binding and no cwd config → structured error naming the flags to add

**Credential references on MCP tools:**
- Accepted: `password_env`, `keychain_service` + `keychain_account` (at most one source per call)
- `connect` rejects inline plaintext `password` with `CAPABILITY_VIOLATION`
- Every tool rejects `password_command` with `CAPABILITY_VIOLATION` — it executes a shell command, so it is CLI-only (a human types it)
- `connect` with `save` (`"local"` | `"global"`) stores the reference, never the secret

---

## Rust Architecture Expectations
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ postgres-native-tls = { version = "0.5", optional = true } # TLS for tokio-post
native-tls = { version = "0.2", optional = true, features = ["vendored"] } # Platform TLS (OpenSSL/Secure Transport/SChannel)
mysql_async = { version = "0.34", default-features = false, features = ["default-rustls"], optional = true } # MySQL native async driver
rusqlite = { version = "0.32", features = ["bundled"], optional = true } # SQLite native driver with bundled lib
duckdb = { version = "1.10504.0", features = ["bundled"], optional = true } # DuckDB native driver with bundled lib
duckdb = { version = "1.10506.0", features = ["bundled"], optional = true } # DuckDB native driver with bundled lib

# BLOB encoding (Base64) - used by SQLite and PostgreSQL
base64 = "0.22"
Expand Down
125 changes: 116 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,70 @@ cargo build --release
- Rust 1.70 or later
- Supported platforms: Linux, macOS, Windows

## Zero to Query

Two paths from nothing to a working query result.

### Path A — CLI-first (recommended for most setups)

**1. Save a connection:**

```bash
# Save a PostgreSQL connection to the current project (password via env var — never plaintext)
plenum connect --name dev --engine postgres \
--host localhost --port 5432 --user myuser \
--password-env PGPASSWORD --database mydb --save local
```

**2. Query immediately:**

```bash
plenum query --sql "SELECT COUNT(*) FROM users"
# => {"ok":true,"engine":"postgres","command":"query","data":{"rows":[{"count":42}]},"meta":{...}}
```

Plenum resolves the `dev` connection automatically from `.plenum/config.json` because it is the default for this project directory.

---

### Path B — MCP-first (for AI coding agents via Claude Desktop or editors)

Register Plenum in your MCP client config with the project path bound at registration time. **You must supply `--project-path`** — see the [cwd-Coupling Warning](#cwd-coupling-warning) below.

**Option 1 — use a saved named connection:**

```json
{
"mcpServers": {
"plenum": {
"command": "plenum",
"args": ["mcp", "--project-path", "/abs/path/to/your/project", "--name", "dev"]
}
}
}
```

Run `plenum connect` first (Path A) to save the connection, then add the stanza above.

**Option 2 — use a DSN from a named env var (no saved config needed):**

```json
{
"mcpServers": {
"plenum": {
"command": "plenum",
"args": ["mcp", "--dsn-env", "DATABASE_URL"]
}
}
}
```

`DATABASE_URL` is read at connection time from that env var only. No ambient credential scanning.

Once the MCP server is running the agent can call the `query` tool without any further setup.

---

## Usage

Plenum provides exactly three commands:
Expand Down Expand Up @@ -355,29 +419,63 @@ Agents should:

## MCP Integration

Plenum exposes functionality via MCP (Model Context Protocol) server:
Plenum exposes all three CLI commands as MCP tools via a local MCP server. The server is started with `plenum mcp` and connection binding is controlled at registration time:

```bash
# Start MCP server (hidden command, for AI agent use)
plenum mcp
plenum mcp --project-path /abs/path/to/project # pin to a project (recommended)
plenum mcp --project-path /abs/path/to/project --name prod # pin to a named connection
plenum mcp --dsn-env DATABASE_URL # read DSN from a named env var
```

Configure in your MCP client:
### cwd-Coupling Warning

When `plenum mcp` is started **without** `--project-path` or `--dsn-env`, connection resolution falls back to the server process's working directory. MCP clients (Claude Desktop, editors, daemons) launch `plenum mcp` from arbitrary directories — often not your project root. The result is a failed resolution, not a wrong connection: the tool call fails with a structured error that names the missing flag to add.

**Always supply `--project-path` or `--dsn-env` in your `mcpServers` stanza.**

### mcpServers configuration

```json
{
"mcpServers": {
"plenum": {
"command": "plenum",
"args": ["mcp"]
"args": ["mcp", "--project-path", "/abs/path/to/your/project", "--name", "dev"]
}
}
}
```

Each CLI command maps to an MCP tool:
- `connect` → Validate and save database connections
- `introspect` → Retrieve schema information
- `query` → Execute constrained SQL queries
### MCP tools

Each CLI command maps to one MCP tool. Tool-call arguments mirror the CLI flags and take precedence over the binding set at server start.

#### `connect` tool

Validates and persists a database connection **by credential reference**. Inline plaintext passwords and `password_command` are rejected with `CAPABILITY_VIOLATION` at the MCP boundary — never pass raw secrets through tool arguments.

| Field | Description |
|-------|-------------|
| `engine` | `postgres`, `mysql`, `sqlite`, or `duckdb` |
| `host`, `port`, `user`, `database` | Server coordinates (postgres/mysql) |
| `file` | File path (sqlite/duckdb) |
| `password_env` | Name of env var holding the password |
| `keychain_service` + `keychain_account` | OS keychain lookup pair (both required together) |
| `save` | `"local"` (`.plenum/config.json`) or `"global"` (`~/.config/plenum/connections.json`) |
| `project_path` | Absolute path of the project to save the connection under |
| `name` | Connection name (defaults to `"default"`) |

At most one credential-reference source (`password_env` or the keychain pair) may be supplied per call.

`password_command` is rejected on every MCP tool because it executes a shell command. Accepting it from tool arguments would let an agent run arbitrary commands on the host. Use `--password-command` from the CLI (`plenum connect`) instead — a human types it there.

#### `introspect` tool

Retrieves schema information. Accepts the same credential-reference fields (`password_env`, `keychain_service`/`keychain_account`) for one-off explicit connections — secrets are resolved at connect time and never appear in tool arguments or logs.

#### `query` tool

Executes a constrained, read-only SQL query. Accepts the same credential-reference fields as `introspect`.

## Architecture

Expand All @@ -402,6 +500,15 @@ Credentials are stored as **plaintext JSON** in config files:
- Secure config files with OS-level permissions (`chmod 600`)
- Avoid `--password` CLI flag (visible in process listings)

#### Credential Sourcing Safety Rules

Plenum's credential sourcing is deterministic and reference-based. These rules hold unconditionally:

1. **Explicit reference only** — The MCP `connect` tool rejects inline plaintext passwords with `CAPABILITY_VIOLATION`. Credentials flow through Plenum only as named references (`password_env`, `keychain_service`/`keychain_account`). MCP tools reject `password_command` because it executes a shell command; it is available only from the CLI. The CLI `--password` flag is available but strongly discouraged (visible in process listings and shell history).
2. **No ambient pickup** — `--dsn-env` reads exactly the one env var you name. Plenum never scans `DATABASE_URL`, `PGPASSWORD`, or any other ambient variable on its own.
3. **No scanning** — Plenum reads only the two fixed config file paths (`.plenum/config.json` and `~/.config/plenum/connections.json`). No directory walking, no inference from arbitrary paths.
4. **Deterministic resolution** — Given the same `--project-path`, `--name`, and env vars, the same connection is always resolved. The precedence order is: explicit CLI flags → local config → global config → structured error.

#### Security Reporting

For detailed security documentation, threat model, and vulnerability reporting, see **[SECURITY.md](SECURITY.md)**.
Expand Down
21 changes: 21 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,27 @@ When using the MCP server, credentials are passed per-invocation via JSON-RPC:

**Security Note**: MCP communication over stdio is local-only (no network exposure).

### MCP Credential References

The MCP `connect` tool rejects an inline plaintext `password` with `CAPABILITY_VIOLATION`. Agents reference the secret instead:

- `password_env` — the name of an environment variable that holds the password.
- `keychain_service` + `keychain_account` — an OS keychain entry (both are required together).

**`password_command` is rejected on every MCP tool** (`connect`, `introspect`, `query`) with `CAPABILITY_VIOLATION`. It executes a shell command via `sh -c`. Accepting it from tool arguments would give any connected agent arbitrary command execution on the host. The CLI flag `--password-command` and the `plenum connect` wizard still offer it, because a human types the command there.

**Residual risk — saved configs:** a connection already saved with `password_command` still runs that command when the MCP server resolves it. The command comes from a config file, not from tool arguments. Review `password_command` entries in `.plenum/config.json` before you commit or pull that file, because it is team-shareable.

### MCP Connection Binding

`plenum mcp` binds its default connection at server start:

- `--project-path <path>` — resolve config for that project, independent of the server's working directory.
- `--name <connection>` — select a named connection within that project.
- `--dsn-env <VAR>` — read the DSN from exactly the named environment variable. Plenum never reads `DATABASE_URL`, `PGPASSWORD`, or any other ambient variable on its own.

`--dsn-env` cannot be combined with `--project-path` or `--name`. Connection fields in a tool call override the binding.

---

## SQL Injection & Query Validation
Expand Down
Loading
Loading