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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ go test -v ./cmd/altinity-mcp/...
- `--clickhouse-username`: Username
- `--clickhouse-password`: Password
- `--clickhouse-protocol`: Protocol (http/tcp)
- `--read-only`: Read-only mode
- `--read-only`: Read-only mode (prevents non-read SQL and avoids setting session variables)
- `--clickhouse-max-execution-time`: Query timeout in seconds
- `--clickhouse-http-headers`: HTTP headers for ClickHouse requests (key=value pairs)

Expand Down
3 changes: 3 additions & 0 deletions pkg/clickhouse/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ func scanRow(rows driver.Rows) ([]interface{}, error) {
// ExecuteQuery executes a SQL query and returns results
// For non-SELECT queries (DDL, DML) will return single row with `OK`
func (c *Client) ExecuteQuery(ctx context.Context, query string, args ...interface{}) (*QueryResult, error) {
if c.config.ReadOnly && !isSelectQuery(query) {
return nil, fmt.Errorf("query rejected: read-only mode allows only SELECT/WITH/SHOW/DESC/EXISTS/EXPLAIN statements")
}
if isSelectQuery(query) {
return c.executeSelect(ctx, query, args...)
}
Expand Down
63 changes: 37 additions & 26 deletions pkg/clickhouse/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,34 +149,45 @@ func TestClientOperations(t *testing.T) {
}

func TestClientErrorPaths(t *testing.T) {
t.Run("ping_failure", func(t *testing.T) {
cfg := &config.ClickHouseConfig{Host: "127.0.0.1", Port: 65000, Database: "default", Username: "default", Protocol: config.TCPProtocol}
ctx := context.Background()
client, err := NewClient(ctx, *cfg)
require.Error(t, err)
require.Nil(t, client)
})
t.Run("ping_failure", func(t *testing.T) {
cfg := &config.ClickHouseConfig{Host: "127.0.0.1", Port: 65000, Database: "default", Username: "default", Protocol: config.TCPProtocol}
ctx := context.Background()
client, err := NewClient(ctx, *cfg)
require.Error(t, err)
require.Nil(t, client)
})

t.Run("describe_table_not_exists", func(t *testing.T) {
cfg := setupClickHouseContainer(t)
ctx := context.Background()
client, err := NewClient(ctx, *cfg)
require.NoError(t, err)
defer func() { _ = client.Close() }()
_, err = client.DescribeTable(ctx, cfg.Database, "not_exists")
require.Error(t, err)
require.Contains(t, err.Error(), "columns not found")
})

t.Run("describe_table_not_exists", func(t *testing.T) {
cfg := setupClickHouseContainer(t)
ctx := context.Background()
client, err := NewClient(ctx, *cfg)
require.NoError(t, err)
defer func() { _ = client.Close() }()
_, err = client.DescribeTable(ctx, cfg.Database, "not_exists")
require.Error(t, err)
require.Contains(t, err.Error(), "columns not found")
})
t.Run("non_select_error", func(t *testing.T) {
cfg := setupClickHouseContainer(t)
ctx := context.Background()
client, err := NewClient(ctx, *cfg)
require.NoError(t, err)
defer func() { _ = client.Close() }()
_, err = client.ExecuteQuery(ctx, "CREATE TABLE broken ENGINE = Memory")
require.Error(t, err)
})

t.Run("non_select_error", func(t *testing.T) {
cfg := setupClickHouseContainer(t)
ctx := context.Background()
client, err := NewClient(ctx, *cfg)
require.NoError(t, err)
defer func() { _ = client.Close() }()
_, err = client.ExecuteQuery(ctx, "CREATE TABLE broken ENGINE = Memory")
require.Error(t, err)
})
t.Run("read_only_blocks_non_select", func(t *testing.T) {
client := &Client{
config: config.ClickHouseConfig{
ReadOnly: true,
},
}
_, err := client.ExecuteQuery(context.Background(), "INSERT INTO t VALUES (1)")
require.Error(t, err)
require.Contains(t, err.Error(), "read-only mode allows only")
})
}

// TestUtilityFunctions tests utility functions
Expand Down
4 changes: 2 additions & 2 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func RegisterTools(srv AltinityMCPServer) {
"properties": map[string]any{
"query": map[string]any{
"type": "string",
"description": "SQL query to execute (SELECT, INSERT, CREATE, etc.)",
"description": "SQL query to execute. In read-only mode, only SELECT/WITH/SHOW/DESC/EXISTS/EXPLAIN are allowed.",
},
"limit": map[string]any{
"type": "number",
Expand Down Expand Up @@ -1014,7 +1014,7 @@ func (s *ClickHouseJWEServer) ServeOpenAPISchema(w http.ResponseWriter, r *http.
"name": "query",
"in": "query",
"required": true,
"description": "SQL to execute (SELECT, INSERT, etc.).",
"description": "SQL to execute. In read-only mode, only SELECT/WITH/SHOW/DESC/EXISTS/EXPLAIN are allowed.",
"schema": map[string]interface{}{"type": "string"},
},
{
Expand Down