Skip to content

Commit fc443fe

Browse files
authored
fix: prevent BrokenResourceError by returning structured responses for query errors (#26)
fixes #25
1 parent 9bd93fc commit fc443fe

File tree

2 files changed

+26
-10
lines changed

2 files changed

+26
-10
lines changed

mcp_clickhouse/mcp_server.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -128,21 +128,36 @@ def execute_query(query: str):
128128
return rows
129129
except Exception as err:
130130
logger.error(f"Error executing query: {err}")
131-
return f"error running query: {err}"
131+
# Return a structured dictionary rather than a string to ensure proper serialization
132+
# by the MCP protocol. String responses for errors can cause BrokenResourceError.
133+
return {"error": str(err)}
132134

133135

134136
@mcp.tool()
135137
def run_select_query(query: str):
136138
"""Run a SELECT query in a ClickHouse database"""
137139
logger.info(f"Executing SELECT query: {query}")
138-
future = QUERY_EXECUTOR.submit(execute_query, query)
139140
try:
140-
result = future.result(timeout=SELECT_QUERY_TIMEOUT_SECS)
141-
return result
142-
except concurrent.futures.TimeoutError:
143-
logger.warning(f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds: {query}")
144-
future.cancel()
145-
return f"Queries taking longer than {SELECT_QUERY_TIMEOUT_SECS} seconds are currently not supported."
141+
future = QUERY_EXECUTOR.submit(execute_query, query)
142+
try:
143+
result = future.result(timeout=SELECT_QUERY_TIMEOUT_SECS)
144+
# Check if we received an error structure from execute_query
145+
if isinstance(result, dict) and "error" in result:
146+
logger.warning(f"Query failed: {result['error']}")
147+
# MCP requires structured responses; string error messages can cause
148+
# serialization issues leading to BrokenResourceError
149+
return {"status": "error", "message": f"Query failed: {result['error']}"}
150+
return result
151+
except concurrent.futures.TimeoutError:
152+
logger.warning(f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds: {query}")
153+
future.cancel()
154+
# Return a properly structured response for timeout errors
155+
return {"status": "error", "message": f"Query timed out after {SELECT_QUERY_TIMEOUT_SECS} seconds"}
156+
except Exception as e:
157+
logger.error(f"Unexpected error in run_select_query: {str(e)}")
158+
# Catch all other exceptions and return them in a structured format
159+
# to prevent MCP serialization failures
160+
return {"status": "error", "message": f"Unexpected error: {str(e)}"}
146161

147162

148163
def create_clickhouse_client():

tests/test_tool.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,9 @@ def test_run_select_query_failure(self):
7171
"""Test running a SELECT query with an error."""
7272
query = f"SELECT * FROM {self.test_db}.non_existent_table"
7373
result = run_select_query(query)
74-
self.assertIsInstance(result, str)
75-
self.assertIn("error running query", result)
74+
self.assertIsInstance(result, dict)
75+
self.assertEqual(result["status"], "error")
76+
self.assertIn("Query failed", result["message"])
7677

7778
def test_table_and_column_comments(self):
7879
"""Test that table and column comments are correctly retrieved."""

0 commit comments

Comments
 (0)