Skip to content

Commit 9f91105

Browse files
make query timeout duration configurable (#89)
* make query timeout duration configurable * fix old references
1 parent 2c96c64 commit 9f91105

File tree

4 files changed

+62
-48
lines changed

4 files changed

+62
-48
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@ The following environment variables are used to configure the ClickHouse and chD
314314
* `CLICKHOUSE_MCP_BIND_PORT`: Port to bind the MCP server to when using HTTP or SSE transport
315315
* Default: `"8000"`
316316
* Only used when transport is `"http"` or `"sse"`
317+
* `CLICKHOUSE_MCP_QUERY_TIMEOUT`: Timeout in seconds for SELECT tools
318+
* Default: `"30"`
319+
* Increase this if you see `Query timed out after ...` errors for heavy queries
317320
* `CLICKHOUSE_ENABLED`: Enable/disable ClickHouse functionality
318321
* Default: `"true"`
319322
* Set to `"false"` to disable ClickHouse tools when using chDB only

mcp_clickhouse/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
from .mcp_server import mcp
2-
from .mcp_env import get_config, TransportType
2+
from .mcp_env import get_mcp_config, TransportType
33

44

55
def main():
6-
config = get_config()
7-
transport = config.mcp_server_transport
6+
mcp_config = get_mcp_config()
7+
transport = mcp_config.server_transport
88

99
# For HTTP and SSE transports, we need to specify host and port
1010
http_transports = [TransportType.HTTP.value, TransportType.SSE.value]
1111
if transport in http_transports:
1212
# Use the configured bind host (defaults to 127.0.0.1, can be set to 0.0.0.0)
1313
# and bind port (defaults to 8000)
14-
mcp.run(transport=transport, host=config.mcp_bind_host, port=config.mcp_bind_port)
14+
mcp.run(transport=transport, host=mcp_config.bind_host, port=mcp_config.bind_port)
1515
else:
1616
# For stdio transport, no host or port is needed
1717
mcp.run(transport=transport)

mcp_clickhouse/mcp_env.py

Lines changed: 46 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ class ClickHouseConfig:
4343
CLICKHOUSE_SEND_RECEIVE_TIMEOUT: Send/receive timeout in seconds (default: 300)
4444
CLICKHOUSE_DATABASE: Default database to use (default: None)
4545
CLICKHOUSE_PROXY_PATH: Path to be added to the host URL. For instance, for servers behind an HTTP proxy (default: None)
46-
CLICKHOUSE_MCP_SERVER_TRANSPORT: MCP server transport method - "stdio", "http", or "sse" (default: stdio)
47-
CLICKHOUSE_MCP_BIND_HOST: Host to bind the MCP server to when using HTTP or SSE transport (default: 127.0.0.1)
48-
CLICKHOUSE_MCP_BIND_PORT: Port to bind the MCP server to when using HTTP or SSE transport (default: 8000)
4946
CLICKHOUSE_ENABLED: Enable ClickHouse server (default: true)
5047
"""
5148

@@ -129,39 +126,6 @@ def send_receive_timeout(self) -> int:
129126
def proxy_path(self) -> str:
130127
return os.getenv("CLICKHOUSE_PROXY_PATH")
131128

132-
@property
133-
def mcp_server_transport(self) -> str:
134-
"""Get the MCP server transport method.
135-
136-
Valid options: "stdio", "http", "sse"
137-
Default: "stdio"
138-
"""
139-
transport = os.getenv("CLICKHOUSE_MCP_SERVER_TRANSPORT", TransportType.STDIO.value).lower()
140-
141-
# Validate transport type
142-
if transport not in TransportType.values():
143-
valid_options = ", ".join(f'"{t}"' for t in TransportType.values())
144-
raise ValueError(f"Invalid transport '{transport}'. Valid options: {valid_options}")
145-
return transport
146-
147-
@property
148-
def mcp_bind_host(self) -> str:
149-
"""Get the host to bind the MCP server to.
150-
151-
Only used when transport is "http" or "sse".
152-
Default: "127.0.0.1"
153-
"""
154-
return os.getenv("CLICKHOUSE_MCP_BIND_HOST", "127.0.0.1")
155-
156-
@property
157-
def mcp_bind_port(self) -> int:
158-
"""Get the port to bind the MCP server to.
159-
160-
Only used when transport is "http" or "sse".
161-
Default: 8000
162-
"""
163-
return int(os.getenv("CLICKHOUSE_MCP_BIND_PORT", "8000"))
164-
165129
def get_client_config(self) -> dict:
166130
"""Get the configuration dictionary for clickhouse_connect client.
167131
@@ -282,3 +246,49 @@ def get_chdb_config() -> ChDBConfig:
282246
if _CHDB_CONFIG_INSTANCE is None:
283247
_CHDB_CONFIG_INSTANCE = ChDBConfig()
284248
return _CHDB_CONFIG_INSTANCE
249+
250+
251+
@dataclass
252+
class MCPServerConfig:
253+
"""Configuration for MCP server-level settings.
254+
255+
These settings control the server transport and tool behavior and are
256+
intentionally independent of ClickHouse connection validation.
257+
258+
Optional environment variables (with defaults):
259+
CLICKHOUSE_MCP_SERVER_TRANSPORT: "stdio", "http", or "sse" (default: stdio)
260+
CLICKHOUSE_MCP_BIND_HOST: Bind host for HTTP/SSE (default: 127.0.0.1)
261+
CLICKHOUSE_MCP_BIND_PORT: Bind port for HTTP/SSE (default: 8000)
262+
CLICKHOUSE_MCP_QUERY_TIMEOUT: SELECT tool timeout in seconds (default: 30)
263+
"""
264+
265+
@property
266+
def server_transport(self) -> str:
267+
transport = os.getenv("CLICKHOUSE_MCP_SERVER_TRANSPORT", TransportType.STDIO.value).lower()
268+
if transport not in TransportType.values():
269+
valid_options = ", ".join(f'"{t}"' for t in TransportType.values())
270+
raise ValueError(f"Invalid transport '{transport}'. Valid options: {valid_options}")
271+
return transport
272+
273+
@property
274+
def bind_host(self) -> str:
275+
return os.getenv("CLICKHOUSE_MCP_BIND_HOST", "127.0.0.1")
276+
277+
@property
278+
def bind_port(self) -> int:
279+
return int(os.getenv("CLICKHOUSE_MCP_BIND_PORT", "8000"))
280+
281+
@property
282+
def query_timeout(self) -> int:
283+
return int(os.getenv("CLICKHOUSE_MCP_QUERY_TIMEOUT", "30"))
284+
285+
286+
_MCP_CONFIG_INSTANCE = None
287+
288+
289+
def get_mcp_config() -> MCPServerConfig:
290+
"""Gets the singleton instance of MCPServerConfig."""
291+
global _MCP_CONFIG_INSTANCE
292+
if _MCP_CONFIG_INSTANCE is None:
293+
_MCP_CONFIG_INSTANCE = MCPServerConfig()
294+
return _MCP_CONFIG_INSTANCE

mcp_clickhouse/mcp_server.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from starlette.requests import Request
1818
from starlette.responses import PlainTextResponse
1919

20-
from mcp_clickhouse.mcp_env import get_config, get_chdb_config
20+
from mcp_clickhouse.mcp_env import get_config, get_chdb_config, get_mcp_config
2121
from mcp_clickhouse.chdb_prompt import CHDB_PROMPT
2222

2323

@@ -63,7 +63,6 @@ class Table:
6363

6464
QUERY_EXECUTOR = concurrent.futures.ThreadPoolExecutor(max_workers=10)
6565
atexit.register(lambda: QUERY_EXECUTOR.shutdown(wait=True))
66-
SELECT_QUERY_TIMEOUT_SECS = 30
6766

6867
load_dotenv()
6968

@@ -186,7 +185,8 @@ def run_select_query(query: str):
186185
try:
187186
future = QUERY_EXECUTOR.submit(execute_query, query)
188187
try:
189-
result = future.result(timeout=SELECT_QUERY_TIMEOUT_SECS)
188+
timeout_secs = get_mcp_config().query_timeout
189+
result = future.result(timeout=timeout_secs)
190190
# Check if we received an error structure from execute_query
191191
if isinstance(result, dict) and "error" in result:
192192
logger.warning(f"Query failed: {result['error']}")
@@ -198,9 +198,9 @@ def run_select_query(query: str):
198198
}
199199
return result
200200
except concurrent.futures.TimeoutError:
201-
logger.warning(f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds: {query}")
201+
logger.warning(f"Query timed out after {timeout_secs} seconds: {query}")
202202
future.cancel()
203-
raise ToolError(f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds")
203+
raise ToolError(f"Query timed out after {timeout_secs} seconds")
204204
except ToolError:
205205
raise
206206
except Exception as e:
@@ -295,7 +295,8 @@ def run_chdb_select_query(query: str):
295295
try:
296296
future = QUERY_EXECUTOR.submit(execute_chdb_query, query)
297297
try:
298-
result = future.result(timeout=SELECT_QUERY_TIMEOUT_SECS)
298+
timeout_secs = get_mcp_config().query_timeout
299+
result = future.result(timeout=timeout_secs)
299300
# Check if we received an error structure from execute_chdb_query
300301
if isinstance(result, dict) and "error" in result:
301302
logger.warning(f"chDB query failed: {result['error']}")
@@ -306,12 +307,12 @@ def run_chdb_select_query(query: str):
306307
return result
307308
except concurrent.futures.TimeoutError:
308309
logger.warning(
309-
f"chDB query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds: {query}"
310+
f"chDB query timed out after {timeout_secs} seconds: {query}"
310311
)
311312
future.cancel()
312313
return {
313314
"status": "error",
314-
"message": f"chDB query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds",
315+
"message": f"chDB query timed out after {timeout_secs} seconds",
315316
}
316317
except Exception as e:
317318
logger.error(f"Unexpected error in run_chdb_select_query: {e}")

0 commit comments

Comments
 (0)