Skip to content

Commit 4cea684

Browse files
authored
fix(readonly): Respect server readonly settings and improve query handling (#35)
- Add helper function `get_readonly_setting` to handle server-client readonly conflicts - Update package version to 0.1.7
1 parent 79290ab commit 4cea684

File tree

4 files changed

+48
-12
lines changed

4 files changed

+48
-12
lines changed

mcp_clickhouse/mcp_env.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ def _validate_required_vars(self) -> None:
133133
missing_vars.append(var)
134134

135135
if missing_vars:
136-
raise ValueError(
137-
f"Missing required environment variables: {', '.join(missing_vars)}"
138-
)
136+
raise ValueError(f"Missing required environment variables: {', '.join(missing_vars)}")
139137

140138

141139
# Global instance placeholder for the singleton pattern

mcp_clickhouse/mcp_server.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def list_tables(database: str, like: str = None):
5656
result = client.command(query)
5757

5858
# Get all table comments in one query
59-
table_comments_query = f"SELECT name, comment FROM system.tables WHERE database = {format_query_value(database)}"
59+
table_comments_query = (
60+
f"SELECT name, comment FROM system.tables WHERE database = {format_query_value(database)}"
61+
)
6062
table_comments_result = client.query(table_comments_query)
6163
table_comments = {row[0]: row[1] for row in table_comments_result.result_rows}
6264

@@ -82,14 +84,16 @@ def get_table_info(table):
8284
for i, col_name in enumerate(column_names):
8385
column_dict[col_name] = row[i]
8486
# Add comment from our pre-fetched comments
85-
if table in column_comments and column_dict['name'] in column_comments[table]:
86-
column_dict['comment'] = column_comments[table][column_dict['name']]
87+
if table in column_comments and column_dict["name"] in column_comments[table]:
88+
column_dict["comment"] = column_comments[table][column_dict["name"]]
8789
else:
88-
column_dict['comment'] = None
90+
column_dict["comment"] = None
8991
columns.append(column_dict)
9092

9193
# Get row count and column count from the table
92-
row_count_query = f"SELECT count() FROM {quote_identifier(database)}.{quote_identifier(table)}"
94+
row_count_query = (
95+
f"SELECT count() FROM {quote_identifier(database)}.{quote_identifier(table)}"
96+
)
9397
row_count_result = client.query(row_count_query)
9498
row_count = row_count_result.result_rows[0][0] if row_count_result.result_rows else 0
9599
column_count = len(columns)
@@ -125,7 +129,8 @@ def get_table_info(table):
125129
def execute_query(query: str):
126130
client = create_clickhouse_client()
127131
try:
128-
res = client.query(query, settings={"readonly": 1})
132+
read_only = get_readonly_setting(client)
133+
res = client.query(query, settings={"readonly": read_only})
129134
column_names = res.column_names
130135
rows = []
131136
for row in res.result_rows:
@@ -161,7 +166,10 @@ def run_select_query(query: str):
161166
logger.warning(f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds: {query}")
162167
future.cancel()
163168
# Return a properly structured response for timeout errors
164-
return {"status": "error", "message": f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds"}
169+
return {
170+
"status": "error",
171+
"message": f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds",
172+
}
165173
except Exception as e:
166174
logger.error(f"Unexpected error in run_select_query: {str(e)}")
167175
# Catch all other exceptions and return them in a structured format
@@ -188,3 +196,33 @@ def create_clickhouse_client():
188196
except Exception as e:
189197
logger.error(f"Failed to connect to ClickHouse: {str(e)}")
190198
raise
199+
200+
201+
def get_readonly_setting(client) -> str:
202+
"""Get the appropriate readonly setting value to use for queries.
203+
204+
This function handles potential conflicts between server and client readonly settings:
205+
- readonly=0: No read-only restrictions
206+
- readonly=1: Only read queries allowed, settings cannot be changed
207+
- readonly=2: Only read queries allowed, settings can be changed (except readonly itself)
208+
209+
If server has readonly=2 and client tries to set readonly=1, it would cause:
210+
"Setting readonly is unknown or readonly" error
211+
212+
This function preserves the server's readonly setting unless it's 0, in which case
213+
we enforce readonly=1 to ensure queries are read-only.
214+
215+
Args:
216+
client: ClickHouse client connection
217+
218+
Returns:
219+
String value of readonly setting to use
220+
"""
221+
read_only = client.server_settings.get("readonly")
222+
if read_only:
223+
if read_only == "0":
224+
return "1" # Force read-only mode if server has it disabled
225+
else:
226+
return read_only.value # Respect server's readonly setting (likely 2)
227+
else:
228+
return "1" # Default to basic read-only mode if setting isn't present

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp-clickhouse"
3-
version = "0.1.6"
3+
version = "0.1.7"
44
description = "An MCP server for ClickHouse."
55
readme = "README.md"
66
license = "Apache-2.0"

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)