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
8 changes: 5 additions & 3 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Validate plugin package
run: python3 -m unittest -v tests/test_plugin_package.py

- uses: googleapis/release-please-action@v4
id: release
with:
token: ${{ secrets.GITHUB_TOKEN }}

- uses: actions/checkout@v4
if: ${{ steps.release.outputs.release_created }}

- name: Create archive
if: ${{ steps.release.outputs.release_created }}
run: |
Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/validate-plugin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Validate plugin

on:
pull_request:
push:
branches:
- main

permissions:
contents: read

jobs:
plugin-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Validate MCP package resources
run: python3 -m unittest -v tests/test_plugin_package.py
4 changes: 3 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ codex plugin add cockroachdb@cockroachdb-codex-plugin
Validate that the marketplace install layout is correct:

```bash
python3 -m unittest -v tests/test_plugin_package.py
REPO_URL="file://$(pwd)" ./scripts/validate-marketplace-install.sh HEAD
```

Expand Down Expand Up @@ -143,7 +144,8 @@ 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.
- Use `env_vars` to forward variables to stdio servers and
`env_http_headers` to source HTTP header values from variables.
- The `tools.yaml` file uses Toolbox v1.1.0 map-based format with `${VAR:default}` syntax for defaults.

### Skills
Expand Down
43 changes: 20 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ CockroachDB plugin for [OpenAI Codex CLI](https://developers.openai.com/codex/).
- `cockroachdb-toolbox` (stdio) — self-hosted [MCP Toolbox](https://mcp-toolbox.dev/integrations/cockroachdb/source/) for any cluster (local dev, self-hosted, or Cloud). Codex spawns the Toolbox process.
- `cockroachdb-toolbox-http` (SSE) — remote/multi-user Toolbox over HTTP.
- **Skills** sourced from [`cockroachlabs/cockroachdb-skills`](https://github.com/cockroachlabs/cockroachdb-skills) — covers query/schema design, observability, security, migrations (MOLT), and cluster lifecycle.
- **Safety hooks** (ship as `hooks.json` + `scripts/`; activation depends on Codex runtime — see Known Limitations):
- **Safety hooks** (ship as `hooks.json` + `scripts/`; activation depends on the Codex runtime version):
- `validate-sql.py` (PreToolUse) — blocks `DROP DATABASE`/`TRUNCATE`, warns on `SERIAL`/multi-DDL.
- `check-sql-files.py` (PostToolUse) — lints SQL/Go/Java/Python/Ruby/JS/TS files for CockroachDB anti-patterns.

Expand Down Expand Up @@ -60,6 +60,10 @@ export COCKROACHDB_DATABASE=defaultdb
export COCKROACHDB_SSLMODE=disable # local dev only; use 'require' or 'verify-full' otherwise
```

Codex starts this backend from the installed plugin root, loads the bundled
`./tools.yaml`, and forwards these variables to Toolbox. No manual MCP
registration or path into the plugin cache is needed.

## Usage

Once installed, Codex auto-discovers the MCP tools. Try:
Expand All @@ -76,10 +80,18 @@ The plugin's skills will be auto-loaded by Codex based on task context.
|---|---|---|
| `cockroachdb-cloud` | HTTP | Managed CockroachDB Cloud MCP. Requires `COCKROACHDB_CLUSTER_ID`. Zero local install. |
| `cockroachdb-toolbox` | stdio | Self-hosted Toolbox against any cluster (local dev, self-hosted, or Cloud). Codex spawns the process. Read-only by default; enable writes via `tools.yaml`. |
| `cockroachdb-toolbox-http` | HTTP/SSE | Remote/multi-user Toolbox deployments. Run `toolbox --config tools.yaml` separately. |
| `cockroachdb-toolbox-http` | HTTP/SSE | Remote/multi-user Toolbox deployments. Client-only; start Toolbox separately. |

Enable/disable per-backend via Codex's MCP toggle UI.

The `cockroachdb-toolbox-http` entry only connects to
`http://127.0.0.1:5000/mcp`; it does not start a server. Before enabling it,
start Toolbox independently from a directory containing your Toolbox config:

```bash
toolbox --config tools.yaml
```

## Troubleshooting

**`toolbox: command not found`** — install [MCP Toolbox](https://mcp-toolbox.dev/documentation/introduction/#install-toolbox) (Homebrew, binary download, or container image).
Expand All @@ -88,6 +100,12 @@ Enable/disable per-backend via Codex's MCP toggle UI.

**Skills not appearing** — verify the installed plugin cache contains skills: `find ~/.codex/plugins/cache/cockroachdb-codex-plugin/cockroachdb/*/skills -name SKILL.md | wc -l`.

**Toolbox uses an unexpected config path** — check `codex mcp list` for a
manually registered `cockroachdb-toolbox` server. A user-level server with the
same name can shadow the plugin-provided server. The plugin itself uses its
installed, bundled `tools.yaml`; it does not require an absolute checkout or
versioned cache path.

**`SSL error: certificate verify failed` or `node is running secure mode, SSL connection required`** — your cluster runs in secure mode. Set:

```bash
Expand All @@ -101,27 +119,6 @@ Then extend `tools.yaml` `queryParams:` block with `sslrootcert: ${COCKROACHDB_S

For a quick local dev cluster, start one in insecure mode: `cockroach start-single-node --insecure --listen-addr=localhost:26257 &` and use `COCKROACHDB_SSLMODE=disable`.

## Known limitations (Codex 0.134.0)

Two Codex runtime gaps affect this plugin until upstream support lands:

1. **Plugin-bundled MCP servers don't get path/env interpolation.** `${PLUGIN_ROOT}` in `.mcp.json` args is passed through literally, and `${VAR}` references in the `env:` block are not expanded. Codex also doesn't inherit `COCKROACHDB_*` env from your shell into spawned MCP processes. Workaround below.
2. **Plugin-bundled `hooks.json` doesn't fire.** Both PreToolUse and PostToolUse hooks are recognized at install time but don't execute against tool calls in 0.134.0. The hooks ship for forward-compatibility.

The HTTP backends (`cockroachdb-toolbox-http`, `cockroachdb-cloud`) are not affected by #1.

**Workaround for #1** — register the toolbox MCP manually with absolute paths and concrete env values:

```bash
codex mcp add cockroachdb-toolbox \
--env COCKROACHDB_HOST=localhost \
--env COCKROACHDB_PORT=26257 \
--env COCKROACHDB_USER=root \
--env COCKROACHDB_DATABASE=defaultdb \
--env COCKROACHDB_SSLMODE=disable \
-- toolbox --config ~/.codex/plugins/cache/cockroachdb-codex-plugin/cockroachdb/0.1.0/tools.yaml --stdio
```

## Contributing

See [`CONTRIBUTING.md`](./CONTRIBUTING.md). Skills are sourced from [`cockroachlabs/cockroachdb-skills`](https://github.com/cockroachlabs/cockroachdb-skills) — open skill PRs there, not in this repo.
Expand Down
21 changes: 11 additions & 10 deletions plugins/cockroachdb/.mcp.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@
"cockroachdb-cloud": {
"type": "http",
"url": "https://cockroachlabs.cloud/mcp",
"headers": {
"mcp-cluster-id": "${COCKROACHDB_CLUSTER_ID}"
"env_http_headers": {
"mcp-cluster-id": "COCKROACHDB_CLUSTER_ID"
}
},
"cockroachdb-toolbox": {
"command": "toolbox",
"args": ["--config", "./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}"
}
"cwd": ".",
"env_vars": [
"COCKROACHDB_HOST",
"COCKROACHDB_PORT",
"COCKROACHDB_USER",
"COCKROACHDB_PASSWORD",
"COCKROACHDB_DATABASE",
"COCKROACHDB_SSLMODE"
]
},
"cockroachdb-toolbox-http": {
"type": "http",
Expand Down
80 changes: 80 additions & 0 deletions tests/test_plugin_package.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import json
import shutil
import tempfile
import unittest
from pathlib import Path


REPO_ROOT = Path(__file__).resolve().parents[1]
PLUGIN_ROOT = REPO_ROOT / "plugins" / "cockroachdb"
MCP_MANIFEST = PLUGIN_ROOT / ".mcp.json"


class PluginMcpPackageTest(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.servers = json.loads(MCP_MANIFEST.read_text(encoding="utf-8"))["mcpServers"]

def test_stdio_toolbox_resolves_bundled_config_from_plugin_root(self) -> None:
server = self.servers["cockroachdb-toolbox"]

self.assertEqual(server["cwd"], ".")
config_flag = server["args"].index("--config")
config_path = server["args"][config_flag + 1]
relative_config = Path(config_path)

self.assertEqual(config_path, "./tools.yaml")
self.assertFalse(relative_config.is_absolute())
self.assertNotIn("..", relative_config.parts)

with tempfile.TemporaryDirectory() as install_parent:
installed_root = Path(install_parent) / "cockroachdb"
shutil.copytree(PLUGIN_ROOT, installed_root)
installed_manifest = json.loads(
(installed_root / ".mcp.json").read_text(encoding="utf-8")
)
installed_server = installed_manifest["mcpServers"]["cockroachdb-toolbox"]
installed_config_flag = installed_server["args"].index("--config")
installed_config = installed_server["args"][installed_config_flag + 1]
resolved_config = (
installed_root / installed_server["cwd"] / installed_config
).resolve()

self.assertEqual(resolved_config, (installed_root / "tools.yaml").resolve())
self.assertTrue(resolved_config.is_file())

def test_stdio_toolbox_forwards_connection_environment(self) -> None:
server = self.servers["cockroachdb-toolbox"]

self.assertNotIn("env", server)
self.assertEqual(
server["env_vars"],
[
"COCKROACHDB_HOST",
"COCKROACHDB_PORT",
"COCKROACHDB_USER",
"COCKROACHDB_PASSWORD",
"COCKROACHDB_DATABASE",
"COCKROACHDB_SSLMODE",
],
)

def test_cloud_mcp_sources_cluster_header_from_environment(self) -> None:
server = self.servers["cockroachdb-cloud"]

self.assertNotIn("headers", server)
self.assertEqual(
server["env_http_headers"],
{"mcp-cluster-id": "COCKROACHDB_CLUSTER_ID"},
)

def test_http_toolbox_remains_an_independent_client_connection(self) -> None:
server = self.servers["cockroachdb-toolbox-http"]

self.assertEqual(server["type"], "http")
self.assertEqual(server["url"], "http://127.0.0.1:5000/mcp")
self.assertNotIn("command", server)


if __name__ == "__main__":
unittest.main()
Loading