From ea6520e3aba0b133e9e7b532926246322626d0d0 Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Fri, 9 Oct 2026 02:17:41 -0400 Subject: [PATCH 1/4] fix: stop shipping the localhost toolbox HTTP server The cockroachdb-toolbox-http entry in .mcp.json pointed at http://127.0.0.1:5000/mcp, so it failed to connect at startup for anyone not already running Toolbox in HTTP mode on that port. Its execute-sql tool also bypassed the SQL safety hook, which only matches the stdio server, and Anthropic's plugin directory validation blocks MCP server URLs that aren't https. The stdio cockroachdb-toolbox server provides the same tools. The README already listed the HTTP backend as an optional alternative; it now shows how to add it with claude mcp add, and the backends table says it isn't shipped. --- .mcp.json | 5 ----- README.md | 6 ++++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.mcp.json b/.mcp.json index 1ab22fe..4dd8f14 100644 --- a/.mcp.json +++ b/.mcp.json @@ -12,11 +12,6 @@ "COCKROACHDB_SSLMODE": "${COCKROACHDB_SSLMODE}" } }, - "cockroachdb-toolbox-http": { - "type": "http", - "url": "http://127.0.0.1:5000/mcp", - "_comment": "SSE transport — run 'toolbox --config tools.yaml' first, then connect via this URL" - }, "cockroachdb-cloud": { "type": "http", "url": "https://cockroachlabs.cloud/mcp", diff --git a/README.md b/README.md index 4d819a5..fb67c18 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ For CockroachDB Cloud, find connection details in the [Cloud Console](https://co ### Alternative MCP Backends -The plugin ships with the **MCP Toolbox** (stdio) backend active by default. To use a different backend, replace the contents of `.mcp.json`: +The plugin ships with the **MCP Toolbox** (stdio) and **CockroachDB Cloud MCP** backends active by default. To add another backend, put its entry in your own MCP configuration (for example a project `.mcp.json`) or add it with `claude mcp add`:
MCP Toolbox via HTTP (remote/multi-user) @@ -60,6 +60,8 @@ The plugin ships with the **MCP Toolbox** (stdio) backend active by default. To } ``` +Or via CLI: `claude mcp add --transport http cockroachdb-toolbox-http http://your-toolbox-host:5000/mcp` + Run Toolbox in HTTP mode: `toolbox --config tools.yaml --address 0.0.0.0 --port 5000` Run Toolbox with the built-in web UI: `toolbox --config tools.yaml --ui --port 5000` (opens at `http://127.0.0.1:5000/ui`) @@ -166,7 +168,7 @@ See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/con |----------------------------|-------------|-----------------|-----------------------------------------------------------------------------------------------------------------------------------| | `cockroachdb-toolbox` | Active | stdio | Any CockroachDB cluster via [MCP Toolbox](https://github.com/googleapis/mcp-toolbox) | | `cockroachdb-cloud` | Active | Streamable HTTP | [Managed MCP Server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) — CockroachDB Cloud (OAuth/API key) | -| `cockroachdb-toolbox-http` | Available | SSE | MCP Toolbox remote/multi-user via HTTP | +| `cockroachdb-toolbox-http` | Available | HTTP | MCP Toolbox remote/multi-user via HTTP (not shipped; add it yourself, see Alternative MCP Backends) | ### CLI Tools From fe058c8d68c5bcbb8a2403f30076008cea8bf0e0 Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Fri, 9 Oct 2026 03:01:28 -0400 Subject: [PATCH 2/4] fix: fall back to defaults when MCP environment variables are unset Claude Code passes an unset bare ${VAR} reference through as the literal text ${VAR}. With COCKROACHDB_* unset, Toolbox received strings like ${COCKROACHDB_PORT} and failed to parse its connection URL, which also defeated the defaults in tools.yaml. The Cloud server's mcp-cluster-id header had the same problem and failed with HTTP 400. The Toolbox env block now uses ${VAR:-default} with the same defaults as tools.yaml. Toolbox uses an empty value as-is rather than falling back, so every connection setting except the password gets a real default. The cluster ID header defaults to empty, which the Cloud server treats as absent, so the connection reaches every cluster the user's role allows. Toolbox runs with readOnlyMode, so the execute-sql tool description, the agent files, and the README no longer promise DDL and DML. The README now covers installing Toolbox on Windows, the running cluster the Toolbox server needs, turning off a server from /mcp, the optional cluster ID, and the first-party CockroachDB MCP Server as an alternative, and it uses the current /plugin install command. CONTRIBUTING and AGENTS no longer recommend bare ${VAR} references and document the scoped tool names that hook matchers need. Fixes #27 --- .mcp.json | 14 ++-- AGENTS.md | 2 + CONTRIBUTING.md | 11 ++-- README.md | 112 ++++++++++++++++++++++++++++---- agents/cockroachdb-dba.md | 2 +- agents/cockroachdb-developer.md | 2 +- agents/cockroachdb-operator.md | 2 +- tools.yaml | 8 ++- 8 files changed, 122 insertions(+), 31 deletions(-) diff --git a/.mcp.json b/.mcp.json index 4dd8f14..cb0e270 100644 --- a/.mcp.json +++ b/.mcp.json @@ -4,19 +4,19 @@ "command": "toolbox", "args": ["--config", "${CLAUDE_PLUGIN_ROOT}/tools.yaml", "--stdio"], "env": { - "COCKROACHDB_HOST": "${COCKROACHDB_HOST}", - "COCKROACHDB_PORT": "${COCKROACHDB_PORT}", - "COCKROACHDB_USER": "${COCKROACHDB_USER}", - "COCKROACHDB_PASSWORD": "${COCKROACHDB_PASSWORD}", - "COCKROACHDB_DATABASE": "${COCKROACHDB_DATABASE}", - "COCKROACHDB_SSLMODE": "${COCKROACHDB_SSLMODE}" + "COCKROACHDB_HOST": "${COCKROACHDB_HOST:-localhost}", + "COCKROACHDB_PORT": "${COCKROACHDB_PORT:-26257}", + "COCKROACHDB_USER": "${COCKROACHDB_USER:-root}", + "COCKROACHDB_PASSWORD": "${COCKROACHDB_PASSWORD:-}", + "COCKROACHDB_DATABASE": "${COCKROACHDB_DATABASE:-defaultdb}", + "COCKROACHDB_SSLMODE": "${COCKROACHDB_SSLMODE:-require}" } }, "cockroachdb-cloud": { "type": "http", "url": "https://cockroachlabs.cloud/mcp", "headers": { - "mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID}" + "mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID:-}" } } } diff --git a/AGENTS.md b/AGENTS.md index 61c78b6..98f0fd4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,8 @@ The CockroachDB plugin for Claude Code: MCP backends (`.mcp.json` + `tools.yaml` - **Never edit `skills/` by hand.** Skills are synced from the [cockroachdb-skills](https://github.com/cockroachlabs/cockroachdb-skills) submodule by a weekly workflow that also regenerates the `skills` array in `.claude-plugin/plugin.json`. Changes there get overwritten. Contribute skills upstream instead. - **Never bump versions by hand.** Release Please owns `version` in `.claude-plugin/plugin.json`, `.release-please-manifest.json`, and `CHANGELOG.md`. Use conventional commits: `fix:`/`feat:` cut a release, `chore:`/`docs:` do not (and that is fine, because installs pull from git, not release tarballs). - **Keep the hook command pattern.** Hook commands load their script through a `runpy` bootstrap with a `\\?\` long-path prefix on Windows and a trailing `; exit 0`. This is load-bearing: it survives plugin cache paths past Windows MAX_PATH (issue #20) and hosts that fail to substitute `${CLAUDE_PLUGIN_ROOT}` (issue #23). A hook must never surface an error on every edit; when the interpreter cannot even open the script, it must fail open. The exact pattern is documented in CONTRIBUTING. +- **Use `${VAR:-default}` in `.mcp.json`, never a bare `${VAR}`.** An unset bare reference reaches the server as literal text (issue #27). Toolbox uses an empty value as-is instead of its default, so connection settings need non-empty defaults that match `tools.yaml`. +- **Match plugin MCP tools by their scoped name.** Claude Code names them `mcp__plugin_cockroachdb___`, and a hook matcher that lists only the bare `mcp____` name never fires for marketplace installs. - **Hook scripts are Python 3 stdlib only**, read JSON on stdin, write JSON on stdout, and always exit 0 (blocking is signaled via `hookSpecificOutput.permissionDecision`, not exit codes). - **No counts in descriptions.** Do not write "three agents" or "33 skills" anywhere; counts go stale. Name the things instead. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75cdc7a..425da66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ claude plugin validate . .claude-plugin/ plugin.json # Plugin manifest (version managed by Release Please) marketplace.json # Marketplace catalog entry -.mcp.json # MCP server definitions (stdio, HTTP, Cloud) +.mcp.json # MCP server definitions (Toolbox stdio, Cloud) tools.yaml # MCP Toolbox source and tool definitions agents/ # Agent markdown files (auto-discovered) hooks/ @@ -139,7 +139,8 @@ This repo uses [Release Please](https://github.com/googleapis/release-please) fo - Hook scripts must be Python 3 with **no external dependencies** (stdlib only). - Read JSON from stdin, write JSON to stdout. -- Exit code 0 = allow/continue; exit code 2 = block the tool call. +- Always exit 0. Signal a block with `hookSpecificOutput.permissionDecision: "deny"`, not with an exit code. +- Tools from the plugin's own MCP servers are named `mcp__plugin_cockroachdb___`. A matcher that names only the bare `mcp____` form never fires for people who installed the plugin, so list the scoped name, and keep the bare name too when the hook should also cover a standalone server. - Load hook scripts through the long-path-safe bootstrap below instead of passing the script path straight to `python3`. On Windows, `${CLAUDE_PLUGIN_ROOT}` resolves to a deeply nested cache path that can exceed the 260-character `MAX_PATH` limit; passing the path directly makes Python fail to open the script and error on every matched tool call (see issue #20). The bootstrap loads the script with `runpy`, prefixing the path with the `\\?\` long-path escape on Windows, keeps it inside single quotes so paths with spaces still work, and uses `; exit 0` so a failed bootstrap never disrupts editing: ```json "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/your-script.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0" @@ -147,8 +148,10 @@ This repo uses [Release Please](https://github.com/googleapis/release-please) fo ### MCP Configuration -- `.mcp.json` defines MCP server backends. -- Use `${ENV_VAR}` syntax for environment variable references. +- `.mcp.json` defines the MCP servers the plugin ships. +- Reference environment variables as `${VAR:-default}`, never as a bare `${VAR}`. Claude Code passes an unset bare reference through as the literal text `${VAR}`, which breaks the server (issue #27). +- Toolbox uses a set-but-empty variable as an empty value rather than falling back to its default, so give each Toolbox connection setting a non-empty default that matches `tools.yaml`. Only `COCKROACHDB_PASSWORD` defaults to empty. +- An optional header uses an empty default, as in `${COCKROACHDB_CLUSTER_ID:-}`. The Cloud MCP server treats an empty `mcp-cluster-id` header as absent. - The `tools.yaml` file uses Toolbox v1.1.0 map-based format with `${VAR:default}` syntax for defaults. ### Skills diff --git a/README.md b/README.md index fb67c18..95e23d8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,14 @@ Connect [Claude Code](https://code.claude.com/) directly to your CockroachDB clu Install from the [Claude Marketplace](https://claude.com/plugins/cockroachdb), or in Claude Code run: ``` -/install-plugin cockroachdb +/plugin install cockroachdb@claude-plugins-official +``` + +To install from this repository's own marketplace instead: + +``` +/plugin marketplace add cockroachdb/claude-plugin +/plugin install cockroachdb@cockroachdb-claude-plugin ``` ### Local development @@ -21,15 +28,44 @@ claude --plugin-dir /path/to/claude-plugin ### Prerequisites -This plugin connects to CockroachDB via MCP (Model Context Protocol) using [MCP Toolbox for Databases](https://github.com/googleapis/mcp-toolbox) (v1.0.0+): +The skills, agents, and hooks work without any setup. Each of the plugin's MCP servers needs its own: + +- **`cockroachdb-toolbox`** (any self-hosted or Cloud cluster) needs [MCP Toolbox for Databases](https://github.com/googleapis/mcp-toolbox) v1.0.0 or later on your `PATH`, and a running CockroachDB cluster it can reach. Toolbox connects when the session starts, so without a reachable cluster this server fails to start. If you don't run a cluster, [turn the server off](#turn-off-a-server-you-dont-use). +- **`cockroachdb-cloud`** (CockroachDB Cloud) needs a CockroachDB Cloud account. Run `/mcp` and select the server to sign in. + +#### Install MCP Toolbox + +macOS or Linux: ```bash brew install mcp-toolbox ``` +Windows: download `toolbox.exe` into a folder on your `PATH`. In PowerShell, set the version to the latest one on the [Toolbox releases page](https://github.com/googleapis/mcp-toolbox/releases), and use `windows/arm64` in the URL on Arm devices: + +```powershell +$VERSION = "1.14.0" +curl.exe -o toolbox.exe "https://storage.googleapis.com/mcp-toolbox-for-databases/v$VERSION/windows/amd64/toolbox.exe" +``` + +Restart Claude after changing your `PATH`, then confirm the install with `toolbox --version`. For other platforms, the container image, and building from source, see [Install Toolbox](https://github.com/googleapis/mcp-toolbox#install-toolbox). + ## Configuration -Set environment variables for your CockroachDB connection: +### Self-hosted clusters (MCP Toolbox) + +The `cockroachdb-toolbox` server reads its connection settings from environment variables. Each one is optional, and an unset variable falls back to its default: + +| Variable | Default | Notes | +|------------------------|-------------|------------------------------------------------------------------------------| +| `COCKROACHDB_HOST` | `localhost` | | +| `COCKROACHDB_PORT` | `26257` | | +| `COCKROACHDB_USER` | `root` | | +| `COCKROACHDB_PASSWORD` | (empty) | | +| `COCKROACHDB_DATABASE` | `defaultdb` | | +| `COCKROACHDB_SSLMODE` | `require` | Use `disable` for a local `--insecure` cluster, `verify-full` for production | + +Set them in the environment Claude starts from. On macOS or Linux, add them to your shell profile: ```bash export COCKROACHDB_HOST="your-cluster-host" @@ -40,7 +76,21 @@ export COCKROACHDB_DATABASE="your-database" export COCKROACHDB_SSLMODE="verify-full" ``` -For CockroachDB Cloud, find connection details in the [Cloud Console](https://cockroachlabs.cloud/). +On Windows, set them as user environment variables, for example `setx COCKROACHDB_HOST "your-cluster-host"`, then restart Claude. + +For a CockroachDB Cloud cluster, find the connection details in the [Cloud Console](https://cockroachlabs.cloud/). + +Toolbox runs in read-only mode: `SELECT`, `SHOW`, and `EXPLAIN` work, and writes and schema changes are rejected. To allow writes, run Toolbox with your own copy of [`tools.yaml`](tools.yaml) that sets `enableWriteMode: true`, configured as your own MCP server, and turn off the plugin's `cockroachdb-toolbox` server. + +### CockroachDB Cloud + +The `cockroachdb-cloud` server connects to the [managed MCP server](https://www.cockroachlabs.com/docs/cockroachcloud/connect-to-the-cockroachdb-cloud-mcp-server) that Cockroach Labs hosts. Run `/mcp`, select `cockroachdb-cloud`, and sign in. The consent screen asks you to grant read access, write access, or both. + +By default the connection can reach every cluster your CockroachDB Cloud role allows. To limit it to one cluster, set `COCKROACHDB_CLUSTER_ID` to that cluster's ID. + +### Turn off a server you don't use + +Claude Code starts both MCP servers in every session. If you only use one of them, open `/mcp`, select the other, and disable it. Claude Code remembers the choice for each project. ### Alternative MCP Backends @@ -69,6 +119,41 @@ Run Toolbox with the built-in web UI: `toolbox --config tools.yaml --ui --port 5 > **Note:** Toolbox must successfully connect to CockroachDB on startup. If the database is unreachable (wrong host/port, env vars not set), the server will hang during initialization and the UI will be stuck on "Fetching tools...". Make sure your `COCKROACHDB_*` environment variables are set and the database is accessible before starting.
+
+CockroachDB MCP Server (first-party, self-hosted) + +[CockroachDB MCP Server](https://github.com/cockroachdb/cockroachdb-mcp-server) is Cockroach Labs' own MCP server for clusters you run yourself. By default it registers only read-only tools, such as `list_databases`, `list_tables`, `get_table_schema`, `select_query`, `explain_query`, `show_statement`, and `show_running_queries`. Setting `CRDB_MCP_ENABLE_WRITE_QUERIES=true` adds `create_database`, `create_table`, `insert_rows`, `update_rows`, and `delete_rows`, and the server refuses an `UPDATE` or `DELETE` without a `WHERE` clause. + +**Install:** `go install github.com/cockroachdb/cockroachdb-mcp-server@latest` (Go 1.26+). Linux and Windows binaries and a Docker image are listed on the [releases page](https://github.com/cockroachdb/cockroachdb-mcp-server/releases). There are no prebuilt macOS binaries, so on macOS use `go install` or Docker. + +**Configure** with certificate authentication (recommended): +```json +{ + "mcpServers": { + "cockroachdb-mcp-server": { + "command": "cockroachdb-mcp-server", + "env": { + "CRDB_HOST": "your-cluster-host", + "CRDB_USERNAME": "ai_agent", + "CRDB_SSL_MODE": "verify-full", + "CRDB_SSL_CA_PATH": "/certs/ca.crt", + "CRDB_SSL_CERTFILE": "/certs/client.ai_agent.crt", + "CRDB_SSL_KEYFILE": "/certs/client.ai_agent.key" + } + } + } +} +``` + +For a local `--insecure` development cluster: + +```bash +claude mcp add cockroachdb-mcp-server -e CRDB_DATABASE_URL="postgresql://root@localhost:26257/defaultdb?sslmode=disable" -e CRDB_MCP_ALLOW_INSECURE_DB=true -- cockroachdb-mcp-server +``` + +Password authentication is off unless you set `CRDB_MCP_ALLOW_PASSWORD_AUTH=true`. Desktop apps may not search `~/go/bin`, so give the binary's absolute path as the `command` if the server doesn't start. The plugin's SQL safety hook applies to the bundled Toolbox server; this server enforces its own guardrails. See the [server's README](https://github.com/cockroachdb/cockroachdb-mcp-server#configuration) for every setting. +
+
ccloud CLI (cluster lifecycle, backups, DR, networking) @@ -122,24 +207,21 @@ ccloud cluster disruption set my-cluster --region us-east-1 --whole-region
CockroachDB Cloud MCP Server (OAuth/API key) -The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) is hosted by Cockroach Labs and requires no infrastructure setup. Authenticate via OAuth 2.1 (PKCE) or a service account API key. Read-only by default; write access requires explicit consent. +The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) is hosted by Cockroach Labs and requires no infrastructure setup. The plugin already includes it as `cockroachdb-cloud`; use these snippets to configure it yourself, for example with a service account API key. With OAuth 2.1 (PKCE), the consent screen asks you to grant read access, write access, or both (scopes `mcp:read` and `mcp:write`). -**OAuth (recommended — opens browser for consent, scopes: `mcp:read`, `mcp:write`):** +**OAuth (recommended, opens a browser for consent):** ```json { "mcpServers": { "cockroachdb-cloud": { "type": "http", - "url": "https://cockroachlabs.cloud/mcp", - "headers": { - "mcp-cluster-id": "{your-cluster-id}" - } + "url": "https://cockroachlabs.cloud/mcp" } } } ``` -**API Key (headless/autonomous agents):** +**API key (headless or autonomous agents):** ```json { "mcpServers": { @@ -147,7 +229,6 @@ The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb "type": "http", "url": "https://cockroachlabs.cloud/mcp", "headers": { - "mcp-cluster-id": "{your-cluster-id}", "Authorization": "Bearer {your-service-account-api-key}" } } @@ -155,7 +236,9 @@ The official [managed MCP server](https://www.cockroachlabs.com/blog/cockroachdb } ``` -Or via CLI: `claude mcp add cockroachdb-cloud https://cockroachlabs.cloud/mcp --transport http --header "mcp-cluster-id: {your-cluster-id}"` +Without an `mcp-cluster-id` header, a connection can reach every cluster your user or service account can access. To limit it to one cluster, add `"mcp-cluster-id": "{your-cluster-id}"` to `headers`. + +Or via CLI: `claude mcp add --transport http cockroachdb-cloud https://cockroachlabs.cloud/mcp`, adding `--header "mcp-cluster-id: {your-cluster-id}"` to limit the connection to one cluster. See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/connect-to-the-cockroachdb-cloud-mcp-server) for detailed setup.
@@ -169,6 +252,7 @@ See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/con | `cockroachdb-toolbox` | Active | stdio | Any CockroachDB cluster via [MCP Toolbox](https://github.com/googleapis/mcp-toolbox) | | `cockroachdb-cloud` | Active | Streamable HTTP | [Managed MCP Server](https://www.cockroachlabs.com/blog/cockroachdb-ai-agents-managed-mcp-server/) — CockroachDB Cloud (OAuth/API key) | | `cockroachdb-toolbox-http` | Available | HTTP | MCP Toolbox remote/multi-user via HTTP (not shipped; add it yourself, see Alternative MCP Backends) | +| `cockroachdb-mcp-server` | Available | stdio, HTTPS | First-party server for self-hosted clusters (not shipped; add it yourself, see Alternative MCP Backends) | ### CLI Tools @@ -182,7 +266,7 @@ See the [quickstart guide](https://www.cockroachlabs.com/docs/cockroachcloud/con | Tool | Description | |----------------------------|--------------------------------------------------| -| `cockroachdb-execute-sql` | Execute SQL statements (SELECT, DDL, DML) | +| `cockroachdb-execute-sql` | Run SQL (read-only unless write mode is enabled) | | `cockroachdb-list-schemas` | List all schemas in the database | | `cockroachdb-list-tables` | List tables with columns, types, and constraints | diff --git a/agents/cockroachdb-dba.md b/agents/cockroachdb-dba.md index cd4c7fe..819b556 100644 --- a/agents/cockroachdb-dba.md +++ b/agents/cockroachdb-dba.md @@ -33,7 +33,7 @@ You are a CockroachDB database administration expert. You specialize in: ## Available MCP Tools **Via MCP Toolbox** (self-hosted, any cluster): -- `cockroachdb-execute-sql`: Execute any SQL statement +- `cockroachdb-execute-sql`: Run SQL (read-only by default: SELECT, SHOW, EXPLAIN; writes and DDL need write mode enabled in the Toolbox config) - `cockroachdb-list-schemas`: List database schemas - `cockroachdb-list-tables`: List tables with column details diff --git a/agents/cockroachdb-developer.md b/agents/cockroachdb-developer.md index 6ff2df9..d64dddb 100644 --- a/agents/cockroachdb-developer.md +++ b/agents/cockroachdb-developer.md @@ -179,7 +179,7 @@ When bulk DML exceeds 250K-500K rows (or 1M+ without secondary indexes): ## Available MCP Tools **Via MCP Toolbox** (self-hosted, any cluster): -- `cockroachdb-execute-sql`: Execute any SQL statement +- `cockroachdb-execute-sql`: Run SQL (read-only by default: SELECT, SHOW, EXPLAIN; writes and DDL need write mode enabled in the Toolbox config) - `cockroachdb-list-schemas`: List database schemas - `cockroachdb-list-tables`: List tables with column details diff --git a/agents/cockroachdb-operator.md b/agents/cockroachdb-operator.md index 707bc02..af8ef6e 100644 --- a/agents/cockroachdb-operator.md +++ b/agents/cockroachdb-operator.md @@ -262,7 +262,7 @@ For batch DML exceeding 250K-500K rows: ## Available MCP Tools **Via MCP Toolbox** (self-hosted, any cluster): -- `cockroachdb-execute-sql`: Execute any SQL statement (diagnostics, SHOW commands, DDL) +- `cockroachdb-execute-sql`: Run SQL for diagnostics and SHOW commands (read-only by default; DDL and other writes need write mode enabled in the Toolbox config) - `cockroachdb-list-schemas`: List database schemas - `cockroachdb-list-tables`: List tables with column details diff --git a/tools.yaml b/tools.yaml index 453c048..1802d2f 100644 --- a/tools.yaml +++ b/tools.yaml @@ -21,9 +21,11 @@ tools: type: cockroachdb-execute-sql source: crdb-source description: | - Execute ad-hoc SQL statements against a CockroachDB database. - Use this for any SQL: SELECT queries, DDL statements (CREATE, ALTER, DROP), - and DML statements (INSERT, UPDATE, DELETE). + Execute an ad-hoc SQL statement against a CockroachDB database. + The connection is read-only by default: SELECT, SHOW, and EXPLAIN work, + while writes and schema changes (INSERT, UPDATE, DELETE, CREATE, ALTER, + DROP) are rejected unless write mode is enabled in the Toolbox + configuration (enableWriteMode: true). cockroachdb-list-schemas: type: cockroachdb-list-schemas From 768317527e770edab937730b15ef878c405f5582 Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Fri, 9 Oct 2026 03:01:28 -0400 Subject: [PATCH 3/4] fix: make the SQL safety hooks run for plugin installs The validate-sql hook never ran for anyone who installed the plugin. Claude Code names tools from a plugin's own MCP servers mcp__plugin____, and the matcher only listed the bare mcp__cockroachdb-toolbox__cockroachdb-execute-sql name, which Claude Code compares as an exact string. The matcher now lists both names. Both hooks also failed to find their scripts on current Claude Code. The bootstrap read ${CLAUDE_PLUGIN_ROOT} from inside single quotes, but Claude Code no longer substitutes the placeholder in shell-form commands. It exports CLAUDE_PLUGIN_ROOT and leaves the shell to expand it, which single quotes prevent, so Python looked for a literal ${CLAUDE_PLUGIN_ROOT}/scripts path and the hook failed open. The bootstrap now uses the substituted path when a host provides one and the exported variable otherwise, and exits quietly when neither leads to the script. The Windows long-path prefix also evaluated to \?\ instead of \\?\. It is now built with chr(92) so no escaping layer can change it. test-hooks.sh gains cases that leave the placeholder unsubstituted and export CLAUDE_PLUGIN_ROOT, in both the sh and PowerShell forms. The old bootstrap fails them. --- hooks/hooks.json | 6 +++--- scripts/test-hooks.sh | 20 +++++++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/hooks/hooks.json b/hooks/hooks.json index 95be03b..36686eb 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -2,11 +2,11 @@ "hooks": { "PreToolUse": [ { - "matcher": "mcp__cockroachdb-toolbox__cockroachdb-execute-sql", + "matcher": "mcp__plugin_cockroachdb_cockroachdb-toolbox__cockroachdb-execute-sql|mcp__cockroachdb-toolbox__cockroachdb-execute-sql", "hooks": [ { "type": "command", - "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/validate-sql.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0", + "command": "python3 -c 'import os, runpy; r = r\"${CLAUDE_PLUGIN_ROOT}\"; r = r if r and not r.startswith(\"$\") else os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\"); p = os.path.normpath(os.path.join(r, \"scripts\", \"validate-sql.py\")) if r else \"\"; p = chr(92) * 2 + \"?\" + chr(92) + p if p and os.name == \"nt\" else p; p and os.path.isfile(p) and runpy.run_path(p, run_name=\"__main__\")'; exit 0", "timeout": 10 } ] @@ -18,7 +18,7 @@ "hooks": [ { "type": "command", - "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/check-sql-files.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0", + "command": "python3 -c 'import os, runpy; r = r\"${CLAUDE_PLUGIN_ROOT}\"; r = r if r and not r.startswith(\"$\") else os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\"); p = os.path.normpath(os.path.join(r, \"scripts\", \"check-sql-files.py\")) if r else \"\"; p = chr(92) * 2 + \"?\" + chr(92) + p if p and os.name == \"nt\" else p; p and os.path.isfile(p) and runpy.run_path(p, run_name=\"__main__\")'; exit 0", "timeout": 15 } ] diff --git a/scripts/test-hooks.sh b/scripts/test-hooks.sh index a714b98..a4769cc 100755 --- a/scripts/test-hooks.sh +++ b/scripts/test-hooks.sh @@ -27,11 +27,17 @@ hook_cmd() { # $1=event $2=root } # check: desc, event, root, stdin, expected_rc, mode(empty|contains), substr +# The command runs with CLAUDE_PLUGIN_ROOT unset unless ENV_ROOT is set, in +# which case it is exported the way Claude Code exports it to hook processes. check() { local desc="$1" event="$2" root="$3" stdin="$4" want_rc="$5" mode="$6" substr="${7:-}" local cmd out rc ok=1 cmd="$(hook_cmd "$event" "$root")" - out="$(printf '%s' "$stdin" | sh -c "$cmd" 2>/dev/null)"; rc=$? + if [ -n "${ENV_ROOT:-}" ]; then + out="$(printf '%s' "$stdin" | CLAUDE_PLUGIN_ROOT="$ENV_ROOT" sh -c "$cmd" 2>/dev/null)"; rc=$? + else + out="$(printf '%s' "$stdin" | env -u CLAUDE_PLUGIN_ROOT sh -c "$cmd" 2>/dev/null)"; rc=$? + fi [ "$rc" = "$want_rc" ] || ok=0 case "$mode" in empty) [ -z "$out" ] || ok=0 ;; @@ -64,6 +70,18 @@ check "non-SQL edit produces no block" PostToolUse "$ROOT" "{\"tool_input\":{\" check "PreToolUse fails open on unsubstituted root" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 empty check "PostToolUse fails open on unsubstituted root" PostToolUse '${CLAUDE_PLUGIN_ROOT}' "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 empty +# --- regression: placeholder left for the shell, root exported (current Claude Code) --- +# Claude Code no longer substitutes ${CLAUDE_PLUGIN_ROOT} inside a shell-form +# command. It exports CLAUDE_PLUGIN_ROOT and lets the shell expand the +# placeholder, which never happens inside the single-quoted bootstrap. In a +# PowerShell shell it rewrites the placeholder to ${env:CLAUDE_PLUGIN_ROOT}, +# which single quotes also leave unexpanded. The bootstrap must read the +# exported variable in both cases. +ENV_ROOT="$ROOT" check "PreToolUse uses the exported root when the placeholder is not substituted" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 contains '"permissionDecision": "deny"' +ENV_ROOT="$ROOT" check "PostToolUse uses the exported root when the placeholder is not substituted" PostToolUse '${CLAUDE_PLUGIN_ROOT}' "{\"tool_input\":{\"file_path\":\"$TMP/a.sql\"}}" 0 contains 'CockroachDB lint' +ENV_ROOT="$ROOT" check "PreToolUse uses the exported root when the placeholder is in PowerShell form" PreToolUse '${env:CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 contains '"permissionDecision": "deny"' +ENV_ROOT="$TMP/missing" check "PreToolUse fails open when the exported root has no script" PreToolUse '${CLAUDE_PLUGIN_ROOT}' '{"tool_input":{"sql":"DROP DATABASE x"}}' 0 empty + echo if [ "$fails" -eq 0 ]; then echo "All hook regression tests passed." From 87688e0917e0cfdd6b3dbbff1d577ed726c1152f Mon Sep 17 00:00:00 2001 From: viragtripathi Date: Fri, 9 Oct 2026 03:13:49 -0400 Subject: [PATCH 4/4] docs: document the fixed hook bootstrap in CONTRIBUTING and AGENTS The documented bootstrap snippet still showed the version that reads ${CLAUDE_PLUGIN_ROOT} from inside single quotes. It now matches hooks/hooks.json, and AGENTS notes the environment-variable fallback as part of the load-bearing pattern. --- AGENTS.md | 2 +- CONTRIBUTING.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 98f0fd4..16cc893 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ The CockroachDB plugin for Claude Code: MCP backends (`.mcp.json` + `tools.yaml` - **Never edit `skills/` by hand.** Skills are synced from the [cockroachdb-skills](https://github.com/cockroachlabs/cockroachdb-skills) submodule by a weekly workflow that also regenerates the `skills` array in `.claude-plugin/plugin.json`. Changes there get overwritten. Contribute skills upstream instead. - **Never bump versions by hand.** Release Please owns `version` in `.claude-plugin/plugin.json`, `.release-please-manifest.json`, and `CHANGELOG.md`. Use conventional commits: `fix:`/`feat:` cut a release, `chore:`/`docs:` do not (and that is fine, because installs pull from git, not release tarballs). -- **Keep the hook command pattern.** Hook commands load their script through a `runpy` bootstrap with a `\\?\` long-path prefix on Windows and a trailing `; exit 0`. This is load-bearing: it survives plugin cache paths past Windows MAX_PATH (issue #20) and hosts that fail to substitute `${CLAUDE_PLUGIN_ROOT}` (issue #23). A hook must never surface an error on every edit; when the interpreter cannot even open the script, it must fail open. The exact pattern is documented in CONTRIBUTING. +- **Keep the hook command pattern.** Hook commands load their script through a `runpy` bootstrap with a `\\?\` long-path prefix on Windows and a trailing `; exit 0`. This is load-bearing: it survives plugin cache paths past Windows MAX_PATH (issue #20), hosts that fail to substitute `${CLAUDE_PLUGIN_ROOT}` (issue #23), and current Claude Code, which exports `CLAUDE_PLUGIN_ROOT` instead of substituting it in shell-form commands, so the bootstrap falls back to the environment variable. A hook must never surface an error on every edit; when the interpreter cannot even open the script, it must fail open. The exact pattern is documented in CONTRIBUTING. - **Use `${VAR:-default}` in `.mcp.json`, never a bare `${VAR}`.** An unset bare reference reaches the server as literal text (issue #27). Toolbox uses an empty value as-is instead of its default, so connection settings need non-empty defaults that match `tools.yaml`. - **Match plugin MCP tools by their scoped name.** Claude Code names them `mcp__plugin_cockroachdb___`, and a hook matcher that lists only the bare `mcp____` name never fires for marketplace installs. - **Hook scripts are Python 3 stdlib only**, read JSON on stdin, write JSON on stdout, and always exit 0 (blocking is signaled via `hookSpecificOutput.permissionDecision`, not exit codes). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 425da66..0e899f1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -141,9 +141,9 @@ This repo uses [Release Please](https://github.com/googleapis/release-please) fo - Read JSON from stdin, write JSON to stdout. - Always exit 0. Signal a block with `hookSpecificOutput.permissionDecision: "deny"`, not with an exit code. - Tools from the plugin's own MCP servers are named `mcp__plugin_cockroachdb___`. A matcher that names only the bare `mcp____` form never fires for people who installed the plugin, so list the scoped name, and keep the bare name too when the hook should also cover a standalone server. -- Load hook scripts through the long-path-safe bootstrap below instead of passing the script path straight to `python3`. On Windows, `${CLAUDE_PLUGIN_ROOT}` resolves to a deeply nested cache path that can exceed the 260-character `MAX_PATH` limit; passing the path directly makes Python fail to open the script and error on every matched tool call (see issue #20). The bootstrap loads the script with `runpy`, prefixing the path with the `\\?\` long-path escape on Windows, keeps it inside single quotes so paths with spaces still work, and uses `; exit 0` so a failed bootstrap never disrupts editing: +- Load hook scripts through the bootstrap below instead of passing the script path straight to `python3`. Claude Code doesn't substitute `${CLAUDE_PLUGIN_ROOT}` inside a shell-form command; it exports `CLAUDE_PLUGIN_ROOT` and leaves the placeholder to the shell, which never expands it inside single quotes. The bootstrap uses the substituted path when a host provides one and otherwise reads the exported variable. On Windows the plugin root can exceed the 260-character `MAX_PATH` limit (issue #20), so the bootstrap prefixes the `\\?\` long-path escape, built with `chr(92)` so no escaping layer can change it. It exits quietly when it can't find the script (issue #23), and `; exit 0` keeps a failed bootstrap from disrupting editing: ```json - "command": "python3 -c 'import sys, os, runpy; p = os.path.normpath(r\"${CLAUDE_PLUGIN_ROOT}/scripts/your-script.py\"); p = (\"\\\\?\\\\\" + p) if os.name == \"nt\" else p; runpy.run_path(p, run_name=\"__main__\")'; exit 0" + "command": "python3 -c 'import os, runpy; r = r\"${CLAUDE_PLUGIN_ROOT}\"; r = r if r and not r.startswith(\"$\") else os.environ.get(\"CLAUDE_PLUGIN_ROOT\", \"\"); p = os.path.normpath(os.path.join(r, \"scripts\", \"your-script.py\")) if r else \"\"; p = chr(92) * 2 + \"?\" + chr(92) + p if p and os.name == \"nt\" else p; p and os.path.isfile(p) and runpy.run_path(p, run_name=\"__main__\")'; exit 0" ``` ### MCP Configuration