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
19 changes: 7 additions & 12 deletions .mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +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-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",
"headers": {
"mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID}"
"mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID:-}"
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ 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_<server>__<tool>`, and a hook matcher that lists only the bare `mcp__<server>__<tool>` 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.

Expand Down
15 changes: 9 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down Expand Up @@ -139,16 +139,19 @@ 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.
- 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:
- 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_<server>__<tool>`. A matcher that names only the bare `mcp__<server>__<tool>` 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 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

- `.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
Expand Down
Loading
Loading