Add run_query_with_params tool for parameterized ClickHouse queries#146
Add run_query_with_params tool for parameterized ClickHouse queries#146FnayouSeif wants to merge 4 commits intoClickHouse:mainfrom
run_query_with_params tool for parameterized ClickHouse queries#146Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new MCP tool, run_query_with_params, to execute parameterized ClickHouse queries via clickhouse-connect’s parameters support, reducing error-prone SQL interpolation (and large value logging).
Changes:
- Extend
execute_query()to accept optional parameters and pass them toclient.query(..., parameters=...). - Add and register a new MCP tool function
run_query_with_params()and export it from the package API. - Add unit/integration tests covering success and several failure modes for parameterized queries.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
mcp_clickhouse/mcp_server.py |
Adds parameter support to the query execution path and registers the new tool. |
mcp_clickhouse/__init__.py |
Exposes run_query_with_params as part of the public API. |
tests/test_mcp_server.py |
Adds MCP-level integration tests for run_query_with_params. |
tests/test_tool.py |
Adds direct (non-MCP) unit tests for run_query_with_params. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
|
|
||
| self.assertIn("Query execution failed", str(context.exception)) | ||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
There are multiple trailing blank lines introduced around this test, which adds noise and makes diffs harder to read. Please remove the extra blank lines to keep formatting consistent with the rest of the file.
| self.assertIn("Query execution failed", str(context.exception)) | |
| self.assertIn("Query execution failed", str(context.exception)) |
| except ToolError: | ||
| raise | ||
| except Exception as e: | ||
| logger.error("Unexpected error in run_query: %s", str(e)) |
There was a problem hiding this comment.
In run_query_with_params, the exception log message says "Unexpected error in run_query" which is misleading and makes it harder to trace issues in logs. Update the log message to reference run_query_with_params (or otherwise identify the tool consistently) so operators can quickly identify the failing path.
| logger.error("Unexpected error in run_query: %s", str(e)) | |
| logger.error("Unexpected error in run_query_with_params: %s", str(e)) |
| # Check if we received an error structure from execute_query | ||
| if isinstance(result, dict) and "error" in result: | ||
| logger.warning(f"Query failed: {result['error']}") | ||
| # MCP requires structured responses; string error messages can cause | ||
| # serialization issues leading to BrokenResourceError | ||
| return { | ||
| "status": "error", | ||
| "message": f"Query failed: {result['error']}", | ||
| } |
There was a problem hiding this comment.
run_query_with_params checks for an { "error": ... } return from execute_query, but execute_query raises ToolError on failure and never returns an error dict. This makes the if isinstance(result, dict) and "error" in result: branch dead code. Either remove this branch here, or change execute_query to return a structured {error: ...} object consistently (and update callers/tests accordingly).
| @pytest.mark.asyncio | ||
| async def test_run_select_query_with_join(mcp_server, setup_test_database): | ||
| """Test running a SELECT query with JOIN.""" | ||
| """Test running a SELECT query with JOIN.""" ##TODO: make this test actually do a join instead of just counting rows in the second table |
There was a problem hiding this comment.
The docstring/comment here includes a TODO about the test not actually doing a join. Leaving knowingly incorrect/misleading test descriptions makes the suite harder to trust. Either implement an actual JOIN in this test or update the docstring to describe what’s really being validated (and move the TODO to an issue/tracker).
| """Test running a SELECT query with JOIN.""" ##TODO: make this test actually do a join instead of just counting rows in the second table | |
| """Test running a SELECT query that counts distinct event types in the second table.""" |
| """Test running a SELECT query with a type mismathc error.""" | ||
|
|
||
| test_db, test_table, _ = setup_test_database | ||
| async with Client(mcp_server) as client: | ||
| # Invalid SQL syntax | ||
| query = f"SELECT id, name, age FROM {test_db}.{test_table} WHERE id = {{id_param:Array(String)}} ORDER BY id" |
There was a problem hiding this comment.
Docstring contains a typo: "mismathc" → "mismatch". Also, the comment below says "Invalid SQL syntax" but the test is about parameter type mismatch; updating the wording will make failures easier to interpret.
|
|
||
| @pytest.mark.asyncio | ||
| async def test_run_select_query_with_params_incorrect_placeholders(mcp_server, setup_test_database): | ||
| """Test running a SELECT query with a type mismathc error.""" |
There was a problem hiding this comment.
This docstring says it’s testing a "type mismathc error" but the test is about missing/incorrect parameter names vs placeholders. Please correct the docstring to reflect the behavior under test (and fix the "mismathc" typo).
| """Test running a SELECT query with a type mismathc error.""" | |
| """Test running a SELECT query with incorrect parameter placeholders or names.""" |
Co-authored-by: Copilot <[email protected]>
Add
run_query_with_paramstool for parameterized ClickHouse queriesMotivation
The existing
run_querytool requires callers to interpolate values directly into the SQL string, which is error-prone and bypasses ClickHouse's type validation. ClickHouse supports parameterized queries natively via{param_name:ClickHouseType}placeholders, andclickhouse-connectexposes this through theparametersargument onclient.query(). This PR surfaces that capability as a dedicated MCP tool.All of that description was AI written. My motivation was the pain of seeing agent writing a vector of 1024 dimension in a SQL and seeing that in the logs.
Changes
mcp_clickhouse/mcp_server.py: Extendedexecute_query()to accept an optionalparams: Dict[str, Any]argument, and addedrun_query_with_params()which passes typed parameters to the ClickHouse driver. The new tool is registered on the MCP server alongsiderun_query.mcp_clickhouse/__init__.py: Exportedrun_query_with_paramsas part of the public API.tests/test_mcp_server.py: Added integration tests covering the happy path, aggregation with params, query errors, syntax errors, type mismatches, and missing/incorrect parameter placeholders.tests/test_tool.py: Added unit tests for the success and failure paths ofrun_query_with_params.Usage
Or via MCP tool call:
{ "tool": "run_query_with_params", "params": { "query": "SELECT * FROM my_db.users WHERE id = {id:UInt32}", "params": { "id": 42 } }}