Skip to content
Open
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
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,37 @@ Or, if you'd like to try it out with the [ClickHouse SQL Playground](https://sql
}
```

For mTLS (mutual TLS) authentication, where client certificates are used instead of a password:

```json
{
"mcpServers": {
"mcp-clickhouse": {
"command": "uv",
"args": [
"run",
"--with",
"mcp-clickhouse",
"--python",
"3.10",
"mcp-clickhouse"
],
"env": {
"CLICKHOUSE_HOST": "<clickhouse-host>",
"CLICKHOUSE_PORT": "<clickhouse-port>",
"CLICKHOUSE_USER": "<clickhouse-user>",
"CLICKHOUSE_SECURE": "true",
"CLICKHOUSE_VERIFY": "true",
"CLICKHOUSE_CLIENT_CERT": "/path/to/client.pem",
"CLICKHOUSE_CLIENT_CERT_KEY": "/path/to/client.key",
"CLICKHOUSE_CONNECT_TIMEOUT": "30",
"CLICKHOUSE_SEND_RECEIVE_TIMEOUT": "30"
}
}
}
}
```

For chDB (embedded ClickHouse engine), add the following configuration:

```json
Expand Down Expand Up @@ -471,6 +502,7 @@ The following environment variables are used to configure the ClickHouse and chD
* `CLICKHOUSE_HOST`: The hostname of your ClickHouse server
* `CLICKHOUSE_USER`: The username for authentication
* `CLICKHOUSE_PASSWORD`: The password for authentication
* Not required when using mTLS client certificate authentication (see `CLICKHOUSE_CLIENT_CERT`)

> [!CAUTION]
> It is important to treat your MCP database user as you would any external client connecting to your database, granting only the minimum necessary privileges required for its operation. The use of default or administrative users should be strictly avoided at all times.
Expand Down Expand Up @@ -536,6 +568,13 @@ The following environment variables are used to configure the ClickHouse and chD
* Only takes effect when `CLICKHOUSE_ALLOW_WRITE_ACCESS=true` is also set
* Set to `"true"` to explicitly allow destructive DROP and TRUNCATE operations
* This is a safety feature to prevent accidental data deletion during AI exploration
* `CLICKHOUSE_CLIENT_CERT`: Path to TLS client certificate for mTLS authentication
* Default: None
* When set, `CLICKHOUSE_PASSWORD` is no longer required
* Must be used with `CLICKHOUSE_CLIENT_CERT_KEY`
* `CLICKHOUSE_CLIENT_CERT_KEY`: Path to TLS client private key for mTLS authentication
* Default: None
* Must be used with `CLICKHOUSE_CLIENT_CERT`
Comment on lines +574 to +577

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README states CLICKHOUSE_CLIENT_CERT “must be used with” CLICKHOUSE_CLIENT_CERT_KEY (and vice versa), but the implementation currently treats these as independently optional and only uses CLICKHOUSE_CLIENT_CERT to waive the password requirement. Either tighten validation to enforce the documented requirement, or relax/reword the README to reflect the actual supported configurations (e.g., combined cert+key PEM).

Suggested change
* Must be used with `CLICKHOUSE_CLIENT_CERT_KEY`
* `CLICKHOUSE_CLIENT_CERT_KEY`: Path to TLS client private key for mTLS authentication
* Default: None
* Must be used with `CLICKHOUSE_CLIENT_CERT`
* Can be used by itself if the PEM file contains both the client certificate and private key
* Use with `CLICKHOUSE_CLIENT_CERT_KEY` when the certificate and private key are stored in separate files
* `CLICKHOUSE_CLIENT_CERT_KEY`: Path to TLS client private key for mTLS authentication
* Default: None
* Optional when `CLICKHOUSE_CLIENT_CERT` points to a combined cert+key PEM file
* Set this when the private key is stored separately from `CLICKHOUSE_CLIENT_CERT`

Copilot uses AI. Check for mistakes.

#### Middleware Variables

Expand Down Expand Up @@ -611,6 +650,17 @@ CLICKHOUSE_ENABLED=false
CHDB_DATA_PATH=/path/to/chdb/data
```

For mTLS (mutual TLS) authentication:

```env
CLICKHOUSE_HOST=your-instance.clickhouse.cloud
CLICKHOUSE_USER=default
CLICKHOUSE_SECURE=true
CLICKHOUSE_CLIENT_CERT=/path/to/client.pem
CLICKHOUSE_CLIENT_CERT_KEY=/path/to/client.key
# CLICKHOUSE_PASSWORD is not required when using client certificates
```

For MCP Inspector or remote access with HTTP transport:

```env
Expand Down
28 changes: 25 additions & 3 deletions mcp_clickhouse/mcp_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ class ClickHouseConfig:
CLICKHOUSE_ENABLED: Enable ClickHouse server (default: true)
CLICKHOUSE_ALLOW_WRITE_ACCESS: Allow write operations (DDL and DML) (default: false)
CLICKHOUSE_ALLOW_DROP: Allow destructive operations (DROP, TRUNCATE) when writes are also enabled (default: false)
CLICKHOUSE_CLIENT_CERT: Path to TLS client certificate for mTLS authentication (default: None)
CLICKHOUSE_CLIENT_CERT_KEY: Path to TLS client private key for mTLS authentication (default: None)
Comment on lines 48 to +52

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class docstring still lists CLICKHOUSE_PASSWORD as a required env var, but _validate_required_vars now makes it optional when client certs are used. Update the docstring’s “Required environment variables” section so it matches the new validation rules (and document the client-cert alternative explicitly).

Copilot uses AI. Check for mistakes.
"""

def __init__(self):
Expand Down Expand Up @@ -86,8 +88,11 @@ def username(self) -> str:

@property
def password(self) -> str:
"""Get the ClickHouse password."""
return os.environ["CLICKHOUSE_PASSWORD"]
"""Get the ClickHouse password.

Returns empty string when not set and client certificates are configured.

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

password now returns an empty string whenever CLICKHOUSE_PASSWORD is unset, but the docstring says that only happens when client certificates are configured. Either adjust the docstring to reflect the actual behavior, or change the implementation to only default to "" when mTLS config is present (and otherwise let validation/KeyError surface).

Suggested change
Returns empty string when not set and client certificates are configured.
Returns an empty string when CLICKHOUSE_PASSWORD is not set.

Copilot uses AI. Check for mistakes.
"""
return os.getenv("CLICKHOUSE_PASSWORD", "")

@property
def role(self) -> Optional[str]:
Expand Down Expand Up @@ -159,6 +164,16 @@ def allow_drop(self) -> bool:
"""
return os.getenv("CLICKHOUSE_ALLOW_DROP", "false").lower() == "true"

@property
def client_cert(self) -> Optional[str]:
"""Get the path to the TLS client certificate for mTLS authentication."""
return os.getenv("CLICKHOUSE_CLIENT_CERT")

@property
def client_cert_key(self) -> Optional[str]:
"""Get the path to the TLS client private key for mTLS authentication."""
return os.getenv("CLICKHOUSE_CLIENT_CERT_KEY")

def get_client_config(self) -> dict:
"""Get the configuration dictionary for clickhouse_connect client.

Expand Down Expand Up @@ -191,6 +206,10 @@ def get_client_config(self) -> dict:

if self.server_host_name:
config["server_host_name"] = self.server_host_name
if self.client_cert:
config["client_cert"] = self.client_cert
if self.client_cert_key:
config["client_cert_key"] = self.client_cert_key

return config

Expand All @@ -201,10 +220,13 @@ def _validate_required_vars(self) -> None:
ValueError: If any required environment variable is missing.
"""
missing_vars = []
for var in ["CLICKHOUSE_HOST", "CLICKHOUSE_USER", "CLICKHOUSE_PASSWORD"]:
for var in ["CLICKHOUSE_HOST", "CLICKHOUSE_USER"]:
if var not in os.environ:
missing_vars.append(var)

if "CLICKHOUSE_PASSWORD" not in os.environ and not self.client_cert:
missing_vars.append("CLICKHOUSE_PASSWORD")

Comment on lines +227 to +229

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current validation only checks CLICKHOUSE_CLIENT_CERT to decide whether CLICKHOUSE_PASSWORD is required, but it doesn’t define/validate what constitutes a complete mTLS configuration (e.g., cert without key, key without cert). This can lead to configs that the README describes as invalid being accepted. Consider validating that the client-cert settings are consistent (and raising a clear ValueError that names the missing counterpart) or updating the docs to match the intended flexibility (e.g., cert file may already include the key).

Copilot uses AI. Check for mistakes.
if missing_vars:
raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")

Expand Down
42 changes: 42 additions & 0 deletions tests/test_config_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,45 @@ def test_server_host_name_omitted_when_unset(monkeypatch: pytest.MonkeyPatch):
client_config = config.get_client_config()

assert "server_host_name" not in client_config


def test_client_cert_configuration(monkeypatch: pytest.MonkeyPatch):
"""Test that client cert and key are passed through when set."""
monkeypatch.setenv("CLICKHOUSE_HOST", "localhost")
monkeypatch.setenv("CLICKHOUSE_USER", "test")
monkeypatch.setenv("CLICKHOUSE_CLIENT_CERT", "/path/to/client.pem")
monkeypatch.setenv("CLICKHOUSE_CLIENT_CERT_KEY", "/path/to/client.key")
monkeypatch.delenv("CLICKHOUSE_PASSWORD", raising=False)

Comment on lines +125 to +132

Copilot AI Apr 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test suite doesn’t cover misconfigured mTLS env combinations (e.g., only CLICKHOUSE_CLIENT_CERT set, only CLICKHOUSE_CLIENT_CERT_KEY set) and what the expected behavior/error should be. Add explicit tests for these partial configurations once the validation rule is clarified, so regressions don’t silently change the authentication requirements.

Copilot uses AI. Check for mistakes.
config = ClickHouseConfig()
client_config = config.get_client_config()

assert client_config["client_cert"] == "/path/to/client.pem"
assert client_config["client_cert_key"] == "/path/to/client.key"
assert client_config["password"] == ""


def test_client_cert_not_in_config_when_unset(monkeypatch: pytest.MonkeyPatch):
"""Test that client cert fields are omitted when not set."""
monkeypatch.setenv("CLICKHOUSE_HOST", "localhost")
monkeypatch.setenv("CLICKHOUSE_USER", "test")
monkeypatch.setenv("CLICKHOUSE_PASSWORD", "test")
monkeypatch.delenv("CLICKHOUSE_CLIENT_CERT", raising=False)
monkeypatch.delenv("CLICKHOUSE_CLIENT_CERT_KEY", raising=False)

config = ClickHouseConfig()
client_config = config.get_client_config()

assert "client_cert" not in client_config
assert "client_cert_key" not in client_config


def test_password_required_without_client_cert(monkeypatch: pytest.MonkeyPatch):
"""Test that CLICKHOUSE_PASSWORD is required when no client cert is set."""
monkeypatch.setenv("CLICKHOUSE_HOST", "localhost")
monkeypatch.setenv("CLICKHOUSE_USER", "test")
monkeypatch.delenv("CLICKHOUSE_PASSWORD", raising=False)
monkeypatch.delenv("CLICKHOUSE_CLIENT_CERT", raising=False)

with pytest.raises(ValueError, match="CLICKHOUSE_PASSWORD"):
ClickHouseConfig()