Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add comprehensive environment configuration handling #11

Merged
merged 3 commits into from
Feb 19, 2025
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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
CLICKHOUSE_PORT: "8123"
CLICKHOUSE_USER: "default"
CLICKHOUSE_PASSWORD: ""
CLICKHOUSE_SECURE: "false"
CLICKHOUSE_VERIFY: "false"
run: |
uv run pytest tests

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.envrc
.ruff_cache/

# Byte-compiled / optimized / DLL files
Expand Down
102 changes: 100 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ An MCP server for ClickHouse.
"CLICKHOUSE_HOST": "<clickhouse-host>",
"CLICKHOUSE_PORT": "<clickhouse-port>",
"CLICKHOUSE_USER": "<clickhouse-user>",
"CLICKHOUSE_PASSWORD": "<clickhouse-password>"
"CLICKHOUSE_PASSWORD": "<clickhouse-password>",
"CLICKHOUSE_SECURE": "true",
"CLICKHOUSE_VERIFY": "true",
"CLICKHOUSE_CONNECT_TIMEOUT": "30",
"CLICKHOUSE_SEND_RECEIVE_TIMEOUT": "30"
}
}
}
Expand Down Expand Up @@ -74,7 +78,11 @@ Or, if you'd like to try it out with the [ClickHouse SQL Playground](https://sql
"CLICKHOUSE_HOST": "sql-clickhouse.clickhouse.com",
"CLICKHOUSE_PORT": "8443",
"CLICKHOUSE_USER": "demo",
"CLICKHOUSE_PASSWORD": ""
"CLICKHOUSE_PASSWORD": "",
"CLICKHOUSE_SECURE": "true",
"CLICKHOUSE_VERIFY": "true",
"CLICKHOUSE_CONNECT_TIMEOUT": "30",
"CLICKHOUSE_SEND_RECEIVE_TIMEOUT": "30"
}
}
}
Expand Down Expand Up @@ -102,3 +110,93 @@ CLICKHOUSE_PASSWORD=clickhouse
3. Run `uv sync` to install the dependencies. To install `uv` follow the instructions [here](https://docs.astral.sh/uv/). Then do `source .venv/bin/activate`.

4. For easy testing, you can run `fastmcp dev mcp_clickhouse/mcp_server.py` to start the MCP server.

### Environment Variables

The following environment variables are used to configure the ClickHouse connection:

#### Required Variables
* `CLICKHOUSE_HOST`: The hostname of your ClickHouse server
* `CLICKHOUSE_USER`: The username for authentication
* `CLICKHOUSE_PASSWORD`: The password for authentication

#### Optional Variables
* `CLICKHOUSE_PORT`: The port number of your ClickHouse server
- Default: `8443` if HTTPS is enabled, `8123` if disabled
- Usually doesn't need to be set unless using a non-standard port
* `CLICKHOUSE_SECURE`: Enable/disable HTTPS connection
- Default: `"true"`
- Set to `"false"` for non-secure connections
* `CLICKHOUSE_VERIFY`: Enable/disable SSL certificate verification
- Default: `"true"`
- Set to `"false"` to disable certificate verification (not recommended for production)
* `CLICKHOUSE_CONNECT_TIMEOUT`: Connection timeout in seconds
- Default: `"30"`
- Increase this value if you experience connection timeouts
* `CLICKHOUSE_SEND_RECEIVE_TIMEOUT`: Send/receive timeout in seconds
- Default: `"300"`
- Increase this value for long-running queries
* `CLICKHOUSE_DATABASE`: Default database to use
- Default: None (uses server default)
- Set this to automatically connect to a specific database

#### Example Configurations

For local development with Docker:
```env
# Required variables
CLICKHOUSE_HOST=localhost
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=clickhouse

# Optional: Override defaults for local development
CLICKHOUSE_SECURE=false # Uses port 8123 automatically
CLICKHOUSE_VERIFY=false
```

For ClickHouse Cloud:
```env
# Required variables
CLICKHOUSE_HOST=your-instance.clickhouse.cloud
CLICKHOUSE_USER=default
CLICKHOUSE_PASSWORD=your-password

# Optional: These use secure defaults
# CLICKHOUSE_SECURE=true # Uses port 8443 automatically
# CLICKHOUSE_DATABASE=your_database
```

For ClickHouse SQL Playground:
```env
CLICKHOUSE_HOST=sql-clickhouse.clickhouse.com
CLICKHOUSE_USER=demo
CLICKHOUSE_PASSWORD=
# Uses secure defaults (HTTPS on port 8443)
```

You can set these variables in your environment, in a `.env` file, or in the Claude Desktop configuration:

```json
{
"mcpServers": {
"mcp-clickhouse": {
"command": "uv",
"args": [
"run",
"--with",
"mcp-clickhouse",
"--python",
"3.13",
"mcp-clickhouse"
],
"env": {
"CLICKHOUSE_HOST": "<clickhouse-host>",
"CLICKHOUSE_USER": "<clickhouse-user>",
"CLICKHOUSE_PASSWORD": "<clickhouse-password>",
"CLICKHOUSE_DATABASE": "<optional-database>"
}
}
}
}
```

141 changes: 141 additions & 0 deletions mcp_clickhouse/mcp_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Environment configuration for the MCP ClickHouse server.

This module handles all environment variable configuration with sensible defaults
and type conversion.
"""

from dataclasses import dataclass
import os
from typing import Optional


@dataclass
class ClickHouseConfig:
"""Configuration for ClickHouse connection settings.

This class handles all environment variable configuration with sensible defaults
and type conversion. It provides typed methods for accessing each configuration value.

Required environment variables:
CLICKHOUSE_HOST: The hostname of the ClickHouse server
CLICKHOUSE_USER: The username for authentication
CLICKHOUSE_PASSWORD: The password for authentication

Optional environment variables (with defaults):
CLICKHOUSE_PORT: The port number (default: 8443 if secure=True, 8123 if secure=False)
CLICKHOUSE_SECURE: Enable HTTPS (default: true)
CLICKHOUSE_VERIFY: Verify SSL certificates (default: true)
CLICKHOUSE_CONNECT_TIMEOUT: Connection timeout in seconds (default: 30)
CLICKHOUSE_SEND_RECEIVE_TIMEOUT: Send/receive timeout in seconds (default: 300)
CLICKHOUSE_DATABASE: Default database to use (default: None)
"""

def __init__(self):
"""Initialize the configuration from environment variables."""
self._validate_required_vars()

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

@property
def port(self) -> int:
"""Get the ClickHouse port.

Defaults to 8443 if secure=True, 8123 if secure=False.
Can be overridden by CLICKHOUSE_PORT environment variable.
"""
if "CLICKHOUSE_PORT" in os.environ:
return int(os.environ["CLICKHOUSE_PORT"])
return 8443 if self.secure else 8123

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

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

@property
def database(self) -> Optional[str]:
"""Get the default database name if set."""
return os.getenv("CLICKHOUSE_DATABASE")

@property
def secure(self) -> bool:
"""Get whether HTTPS is enabled.

Default: True
"""
return os.getenv("CLICKHOUSE_SECURE", "true").lower() == "true"

@property
def verify(self) -> bool:
"""Get whether SSL certificate verification is enabled.

Default: True
"""
return os.getenv("CLICKHOUSE_VERIFY", "true").lower() == "true"

@property
def connect_timeout(self) -> int:
"""Get the connection timeout in seconds.

Default: 30
"""
return int(os.getenv("CLICKHOUSE_CONNECT_TIMEOUT", "30"))

@property
def send_receive_timeout(self) -> int:
"""Get the send/receive timeout in seconds.

Default: 300 (ClickHouse default)
"""
return int(os.getenv("CLICKHOUSE_SEND_RECEIVE_TIMEOUT", "300"))

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

Returns:
dict: Configuration ready to be passed to clickhouse_connect.get_client()
"""
config = {
"host": self.host,
"port": self.port,
"username": self.username,
"password": self.password,
"secure": self.secure,
"verify": self.verify,
"connect_timeout": self.connect_timeout,
"send_receive_timeout": self.send_receive_timeout,
}

# Add optional database if set
if self.database:
config["database"] = self.database

return config

def _validate_required_vars(self) -> None:
"""Validate that all required environment variables are set.

Raises:
ValueError: If any required environment variable is missing.
"""
missing_vars = []
for var in ["CLICKHOUSE_HOST", "CLICKHOUSE_USER", "CLICKHOUSE_PASSWORD"]:
if var not in os.environ:
missing_vars.append(var)

if missing_vars:
raise ValueError(
f"Missing required environment variables: {', '.join(missing_vars)}"
)


# Global instance for easy access
config = ClickHouseConfig()
29 changes: 19 additions & 10 deletions mcp_clickhouse/mcp_server.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import logging
import os
from typing import Sequence

import clickhouse_connect
from dotenv import load_dotenv
from fastmcp import FastMCP

from mcp_clickhouse.mcp_env import config

MCP_SERVER_NAME = "mcp-clickhouse"

# Configure logging
Expand Down Expand Up @@ -102,13 +103,21 @@ def run_select_query(query: str):


def create_clickhouse_client():
host = os.getenv("CLICKHOUSE_HOST")
port = os.getenv("CLICKHOUSE_PORT")
username = os.getenv("CLICKHOUSE_USER")
logger.info(f"Creating ClickHouse client connection to {host}:{port} as {username}")
return clickhouse_connect.get_client(
host=host,
port=port,
username=username,
password=os.getenv("CLICKHOUSE_PASSWORD"),
client_config = config.get_client_config()
logger.info(
f"Creating ClickHouse client connection to {client_config['host']}:{client_config['port']} "
f"as {client_config['username']} "
f"(secure={client_config['secure']}, verify={client_config['verify']}, "
f"connect_timeout={client_config['connect_timeout']}s, "
f"send_receive_timeout={client_config['send_receive_timeout']}s)"
)

try:
client = clickhouse_connect.get_client(**client_config)
# Test the connection
version = client.server_version
logger.info(f"Successfully connected to ClickHouse server version {version}")
return client
except Exception as e:
logger.error(f"Failed to connect to ClickHouse: {str(e)}")
raise