Skip to content
79 changes: 79 additions & 0 deletions docs/CLICKHOUSE_README.md
Copy link
Contributor

Choose a reason for hiding this comment

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

This file might not be necessary. It was created for use with GeminiCLI or Antigravity.

Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# ClickHouse MCP Server

The ClickHouse Model Context Protocol (MCP) Server enables AI-powered development tools to seamlessly connect, interact, and generate data insights with your ClickHouse databases using natural language commands.

## Features

An editor configured to use the ClickHouse MCP server can use its AI capabilities to help you:

- **Natural Language to Data Analytics:** Easily find required ClickHouse tables and ask analytical questions in plain English.
- **Seamless Workflow:** Stay within your CLI, eliminating the need to constantly switch to the ClickHouse console for generating analytical insights.
- **Execute SQL Queries:** Run parameterized SQL queries and prepared statements against your ClickHouse databases.


## Server Capabilities

The ClickHouse MCP server provides the following tools:

| Tool Name | Description |
|:-----------------------|:----------------------------------------------------------------|
| `execute_sql` | Executes a SQL query against ClickHouse. |
| `list_databases` | Lists all databases in the ClickHouse instance. |
| `list_tables` | Lists all tables in a specific ClickHouse database. |

## Custom MCP Server Configuration

The ClickHouse MCP server is configured using environment variables.

```bash
export CLICKHOUSE_HOST="<your-clickhouse-host>"
export CLICKHOUSE_PORT="<your-clickhouse-port>" # e.g., "8123" for HTTP, "9000" for native
export CLICKHOUSE_USER="<your-clickhouse-user>"
export CLICKHOUSE_PASSWORD="<your-clickhouse-password>"
export CLICKHOUSE_DATABASE="<your-database-name>"
export CLICKHOUSE_PROTOCOL="https" # Optional: "http" or "https" (default: "https")
```

Add the following configuration to your MCP client (e.g., `settings.json` for Gemini CLI, `mcp_config.json` for Antigravity):

```json
{
"mcpServers": {
"clickhouse": {
"command": "npx",
"args": ["-y", "@toolbox-sdk/server", "--prebuilt", "clickhouse", "--stdio"],
"env": {
"CLICKHOUSE_HOST": "<your-clickhouse-host>",
"CLICKHOUSE_PORT": "<your-clickhouse-port>",
"CLICKHOUSE_USER": "<your-clickhouse-user>",
"CLICKHOUSE_PASSWORD": "<your-clickhouse-password>",
"CLICKHOUSE_DATABASE": "<your-database-name>",
"CLICKHOUSE_PROTOCOL": "https"
}
}
}
}
```

### Advanced Configuration

You can also configure connection pool settings in your `tools.yaml` file:


```yaml
sources:
my-clickhouse-source:
kind: clickhouse
host: ${CLICKHOUSE_HOST}
port: ${CLICKHOUSE_PORT}
database: ${CLICKHOUSE_DATABASE}
user: ${CLICKHOUSE_USER}
password: ${CLICKHOUSE_PASSWORD}
protocol: https # Optional: http or https (default: https)
secure: true # Optional: boolean (default: false)
# Optional connection pool settings
maxOpenConns: 50 # Optional: Maximum number of open connections (default: 25)
maxIdleConns: 10 # Optional: Maximum number of idle connections (default: 5)
connMaxLifetime: 10m # Optional: Maximum connection lifetime (default: 5m)
# Accepts duration strings like "30s", "5m", "1h", etc.
```
72 changes: 53 additions & 19 deletions internal/sources/clickhouse/clickhouse.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,15 @@ import (

const SourceKind string = "clickhouse"

const (
// DefaultMaxOpenConns is the default maximum number of open connections to the database.
DefaultMaxOpenConns = 25
// DefaultMaxIdleConns is the default maximum number of idle connections in the pool.
DefaultMaxIdleConns = 5
// DefaultConnMaxLifetime is the default maximum lifetime of a connection.
DefaultConnMaxLifetime = 5 * time.Minute
)

// validate interface
var _ sources.SourceConfig = Config{}

Expand All @@ -47,23 +56,26 @@ func newConfig(ctx context.Context, name string, decoder *yaml.Decoder) (sources
}

type Config struct {
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Host string `yaml:"host" validate:"required"`
Port string `yaml:"port" validate:"required"`
Database string `yaml:"database" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password"`
Protocol string `yaml:"protocol"`
Secure bool `yaml:"secure"`
Name string `yaml:"name" validate:"required"`
Kind string `yaml:"kind" validate:"required"`
Host string `yaml:"host" validate:"required"`
Port string `yaml:"port" validate:"required"`
Database string `yaml:"database" validate:"required"`
User string `yaml:"user" validate:"required"`
Password string `yaml:"password"`
Protocol string `yaml:"protocol"`
Secure bool `yaml:"secure"`
MaxOpenConns *int `yaml:"maxOpenConns" validate:"omitempty,gt=0"`
MaxIdleConns *int `yaml:"maxIdleConns" validate:"omitempty,gt=0"`
ConnMaxLifetime string `yaml:"connMaxLifetime"`
}

func (r Config) SourceConfigKind() string {
return SourceKind
}

func (r Config) Initialize(ctx context.Context, tracer trace.Tracer) (sources.Source, error) {
pool, err := initClickHouseConnectionPool(ctx, tracer, r.Name, r.Host, r.Port, r.User, r.Password, r.Database, r.Protocol, r.Secure)
pool, err := initClickHouseConnectionPool(ctx, tracer, r)
if err != nil {
return nil, fmt.Errorf("unable to create pool: %w", err)
}
Expand Down Expand Up @@ -108,11 +120,12 @@ func validateConfig(protocol string) error {
return nil
}

func initClickHouseConnectionPool(ctx context.Context, tracer trace.Tracer, name, host, port, user, pass, dbname, protocol string, secure bool) (*sql.DB, error) {
func initClickHouseConnectionPool(ctx context.Context, tracer trace.Tracer, config Config) (*sql.DB, error) {
//nolint:all // Reassigned ctx
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, name)
ctx, span := sources.InitConnectionSpan(ctx, tracer, SourceKind, config.Name)
defer span.End()

protocol := config.Protocol
if protocol == "" {
protocol = "https"
}
Expand All @@ -121,15 +134,15 @@ func initClickHouseConnectionPool(ctx context.Context, tracer trace.Tracer, name
return nil, err
}

encodedUser := url.QueryEscape(user)
encodedPass := url.QueryEscape(pass)
encodedUser := url.QueryEscape(config.User)
encodedPass := url.QueryEscape(config.Password)

var dsn string
scheme := protocol
if protocol == "http" && secure {
if protocol == "http" && config.Secure {
scheme = "https"
}
dsn = fmt.Sprintf("%s://%s:%s@%s:%s/%s", scheme, encodedUser, encodedPass, host, port, dbname)
dsn = fmt.Sprintf("%s://%s:%s@%s:%s/%s", scheme, encodedUser, encodedPass, config.Host, config.Port, config.Database)
if scheme == "https" {
dsn += "?secure=true&skip_verify=false"
}
Expand All @@ -139,9 +152,30 @@ func initClickHouseConnectionPool(ctx context.Context, tracer trace.Tracer, name
return nil, fmt.Errorf("sql.Open: %w", err)
}

pool.SetMaxOpenConns(25)
pool.SetMaxIdleConns(5)
pool.SetConnMaxLifetime(5 * time.Minute)
// Set MaxOpenConns with default value if not specified
maxOpen := DefaultMaxOpenConns
if config.MaxOpenConns != nil {
maxOpen = *config.MaxOpenConns
}
pool.SetMaxOpenConns(maxOpen)

// Set MaxIdleConns with default value if not specified
maxIdle := DefaultMaxIdleConns
if config.MaxIdleConns != nil {
maxIdle = *config.MaxIdleConns
}
pool.SetMaxIdleConns(maxIdle)

// Set ConnMaxLifetime with default value if not specified
connLifetime := DefaultConnMaxLifetime
if config.ConnMaxLifetime != "" {
parsedLifetime, err := time.ParseDuration(config.ConnMaxLifetime)
if err != nil {
return nil, fmt.Errorf("invalid connMaxLifetime %q: %w", config.ConnMaxLifetime, err)
}
connLifetime = parsedLifetime
}
pool.SetConnMaxLifetime(connLifetime)

return pool, nil
}
Loading