diff --git a/src/tools/__init__.py b/src/tools/__init__.py index 8ac42a5..352d458 100644 --- a/src/tools/__init__.py +++ b/src/tools/__init__.py @@ -4,6 +4,9 @@ This module contains all the MCP tools for Couchbase operations. """ +# Index tools +from .index import get_index_advisor_recommendations + # Key-Value tools from .kv import ( delete_document_by_id, @@ -40,6 +43,7 @@ delete_document_by_id, get_schema_for_collection, run_sql_plus_plus_query, + get_index_advisor_recommendations, ] __all__ = [ @@ -55,6 +59,7 @@ "delete_document_by_id", "get_schema_for_collection", "run_sql_plus_plus_query", + "get_index_advisor_recommendations", # Convenience "ALL_TOOLS", ] diff --git a/src/tools/index.py b/src/tools/index.py new file mode 100644 index 0000000..841126d --- /dev/null +++ b/src/tools/index.py @@ -0,0 +1,84 @@ +""" +Tools for index operations and optimization. + +This module contains tools for getting index recommendations using the Couchbase Index Advisor. +""" + +import logging +from typing import Any + +from mcp.server.fastmcp import Context + +from tools.query import run_cluster_query +from utils.constants import MCP_SERVER_NAME + +logger = logging.getLogger(f"{MCP_SERVER_NAME}.tools.index") + + +def get_index_advisor_recommendations(ctx: Context, query: str) -> dict[str, Any]: + """Get index recommendations from Couchbase Index Advisor for a given SQL++ query. + + The Index Advisor analyzes the query and provides recommendations for optimal indexes. + This tool works with SELECT, UPDATE, DELETE, or MERGE queries. + The query should contain fully qualified keyspace (e.g., bucket.scope.collection). + + Returns a dictionary with: + - current_used_indexes: Array of currently used indexes (if any) + - recommended_indexes: Array of recommended secondary indexes (if any) + - recommended_covering_indexes: Array of recommended covering indexes (if any) + + Each index object contains: + - index: The CREATE INDEX SQL++ command + - statements: Array of statement objects with the query and run count + """ + try: + # Build the ADVISOR query + advisor_query = f"SELECT ADVISOR('{query}') AS advisor_result" + + logger.info("Running Index Advisor for the provided query") + + # Execute the ADVISOR function at cluster level using run_cluster_query + advisor_results = run_cluster_query(ctx, advisor_query) + + if not advisor_results: + return { + "message": "No recommendations available", + "current_used_indexes": [], + "recommended_indexes": [], + "recommended_covering_indexes": [], + } + + # The result is wrapped in advisor_result key + advisor_data = advisor_results[0].get("advisor_result", {}) + + # Extract the relevant fields with defaults + response = { + "current_used_indexes": advisor_data.get("current_used_indexes", []), + "recommended_indexes": advisor_data.get("recommended_indexes", []), + "recommended_covering_indexes": advisor_data.get( + "recommended_covering_indexes", [] + ), + } + + # Add summary information for better user experience + response["summary"] = { + "current_indexes_count": len(response["current_used_indexes"]), + "recommended_indexes_count": len(response["recommended_indexes"]), + "recommended_covering_indexes_count": len( + response["recommended_covering_indexes"] + ), + "has_recommendations": bool( + response["recommended_indexes"] + or response["recommended_covering_indexes"] + ), + } + + logger.info( + f"Index Advisor completed. Found {response['summary']['recommended_indexes_count']} recommended indexes" + ) + + return response + + except Exception as e: + logger.error(f"Error running Index Advisor: {e!s}", exc_info=True) + raise