Skip to content

Commit d7566c2

Browse files
rekbyCopilot
andauthored
add-query-explain (#13)
* Add support for disabling automatic output‑schema detection (for compatibility with the MCP package ≥ 1.10), and prevent the MCP server from auto‑updating going forward. * Update ydb_mcp/server.py Co-authored-by: Copilot <[email protected]> * fix style * add explain_query and explain_query_with_params tools --------- Co-authored-by: Copilot <[email protected]>
1 parent 3ef7fe0 commit d7566c2

File tree

3 files changed

+116
-11
lines changed

3 files changed

+116
-11
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
* Add explain_query and explain_query_with_params tools
2+
13
## 0.1.5 ##
24
* Fix error outputSchema defined but no structured output returned
35

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
ydb>=3.21.0
1+
ydb>=3.21.9
22
mcp==1.12.4

ydb_mcp/server.py

Lines changed: 113 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,78 @@ def _stringify_dict_keys(self, obj):
356356
else:
357357
return obj
358358

359+
async def explain_query(self, sql: str, params: Optional[Dict[str, Any]] = None) -> List[TextContent]:
360+
"""Explain a SQL query against YDB
361+
362+
Args:
363+
sql: SQL query to execute
364+
params: Optional query parameters
365+
366+
Returns:
367+
Execution plan of the query as TextContent object with JSON-formatted execution plan
368+
"""
369+
# Check if there's an authentication error
370+
if self.auth_error:
371+
return [TextContent(type="text", text=json.dumps({"error": self.auth_error}, indent=2))]
372+
373+
try:
374+
pool = await self.get_pool()
375+
ydb_params = None
376+
if params:
377+
ydb_params = {}
378+
for key, value in params.items():
379+
param_key = key if key.startswith("$") else f"${key}"
380+
ydb_params[param_key] = value
381+
382+
structured_plan = await pool.explain_with_retries(
383+
query=sql,
384+
parameters=ydb_params,
385+
result_format=ydb.QueryExplainResultFormat.DICT,
386+
)
387+
388+
safe_plan = self._stringify_dict_keys(structured_plan)
389+
formatted_plan = json.dumps(safe_plan, indent=2, cls=CustomJSONEncoder)
390+
logger.info(f"Query plan: {formatted_plan}")
391+
return [TextContent(type="text", text=formatted_plan)]
392+
except Exception as e:
393+
error_message = str(e)
394+
safe_error = self._stringify_dict_keys({"error": error_message})
395+
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
396+
397+
async def explain_query_with_params(self, sql: str, params: str) -> List[TextContent]:
398+
"""Explain a SQL query against YDB
399+
400+
Args:
401+
sql: SQL query to execute
402+
params: Optional query parameters
403+
404+
Returns:
405+
Execution plan of the query as TextContent object with JSON-formatted execution plan
406+
"""
407+
"""Run a parameterized SQL query with JSON parameters.
408+
409+
Args:
410+
sql: SQL query to execute
411+
params: Parameters as a JSON string
412+
413+
Returns:
414+
Query results as a list of TextContent objects or a dictionary
415+
"""
416+
# Handle authentication errors
417+
if self.auth_error:
418+
logger.error(f"Authentication error: {self.auth_error}")
419+
safe_error = self._stringify_dict_keys({"error": f"Authentication error: {self.auth_error}"})
420+
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
421+
422+
try:
423+
ydb_params = self._parse_str_to_ydb_params(params)
424+
except json.JSONDecodeError as e:
425+
logger.error(f"Error parsing JSON parameters: {str(e)}")
426+
safe_error = self._stringify_dict_keys({"error": f"Error parsing JSON parameters: {str(e)}"})
427+
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
428+
429+
return await self.explain_query(sql, ydb_params)
430+
359431
async def query(self, sql: str, params: Optional[Dict[str, Any]] = None) -> List[TextContent]:
360432
"""Run a SQL query against YDB.
361433
@@ -444,14 +516,25 @@ async def query_with_params(self, sql: str, params: str) -> List[TextContent]:
444516
logger.error(f"Authentication error: {self.auth_error}")
445517
safe_error = self._stringify_dict_keys({"error": f"Authentication error: {self.auth_error}"})
446518
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
447-
parsed_params = {}
448519
try:
449-
if params and params.strip():
450-
parsed_params = json.loads(params)
520+
ydb_params = self._parse_str_to_ydb_params(params)
521+
522+
return await self.query(sql, ydb_params)
451523
except json.JSONDecodeError as e:
452524
logger.error(f"Error parsing JSON parameters: {str(e)}")
453525
safe_error = self._stringify_dict_keys({"error": f"Error parsing JSON parameters: {str(e)}"})
454526
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
527+
except Exception as e:
528+
error_message = f"Error executing parameterized query: {str(e)}"
529+
logger.error(error_message)
530+
safe_error = self._stringify_dict_keys({"error": error_message})
531+
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
532+
533+
def _parse_str_to_ydb_params(self, params: str) -> Dict:
534+
parsed_params = {}
535+
if params and params.strip():
536+
parsed_params = json.loads(params)
537+
455538
# Convert [value, type] to YDB type if needed
456539
ydb_params = {}
457540
for key, value in parsed_params.items():
@@ -465,13 +548,9 @@ async def query_with_params(self, sql: str, params: str) -> List[TextContent]:
465548
ydb_params[param_key] = param_value
466549
else:
467550
ydb_params[param_key] = value
468-
try:
469-
return await self.query(sql, ydb_params)
470-
except Exception as e:
471-
error_message = f"Error executing parameterized query: {str(e)}"
472-
logger.error(error_message)
473-
safe_error = self._stringify_dict_keys({"error": error_message})
474-
return [TextContent(type="text", text=json.dumps(safe_error, indent=2))]
551+
552+
return ydb_params
553+
475554

476555
def register_tools(self):
477556
"""Register YDB query tools.
@@ -483,6 +562,30 @@ def register_tools(self):
483562
"""
484563
# Define tool specifications
485564
tool_specs = [
565+
{
566+
"name": "ydb_explain_query",
567+
"description": "Explain a SQL query against YDB",
568+
"handler": self.explain_query, # Use real handler
569+
"parameters": {
570+
"properties": {"sql": {"type": "string", "title": "Sql"}},
571+
"required": ["sql"],
572+
"type": "object",
573+
},
574+
},
575+
{
576+
"name": "ydb_explain_query_with_params",
577+
"description": "Explain a parametrized SQL query with JSON parameters",
578+
"handler": self.explain_query_with_params, # Use real handler
579+
"parameters": {
580+
"properties": {
581+
"sql": {"type": "string", "title": "Sql"},
582+
"params": {"type": "string", "title": "Params"},
583+
},
584+
"required": ["sql", "params"],
585+
"type": "object",
586+
},
587+
588+
},
486589
{
487590
"name": "ydb_query",
488591
"description": "Run a SQL query against YDB database",

0 commit comments

Comments
 (0)