From af6a09f26eebef01f9da37b3b455ef21f40582a6 Mon Sep 17 00:00:00 2001 From: AayushTyagi1 Date: Thu, 16 Oct 2025 12:14:57 +0530 Subject: [PATCH 1/6] DA-1171: Added list Indexes with Definitions --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 83716f6..89babac 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ An [MCP](https://modelcontextprotocol.io/) server implementation of Couchbase th - Get a list of all the scopes in the specified bucket - Get a list of all the collections in a specified scope and bucket. Note that this tool requires the cluster to have Query service. - Get the structure for a collection +- List all indexes in the cluster with their definitions, optionally filtered by bucket name - Get a document by ID from a specified scope and collection - Upsert a document by ID to a specified scope and collection - Delete a document by ID from a specified scope and collection From 06bb213c3617b5e462019419b89c16894f512cd9 Mon Sep 17 00:00:00 2001 From: AayushTyagi1 Date: Thu, 16 Oct 2025 12:15:42 +0530 Subject: [PATCH 2/6] DA-1171: Added list Indexes with Definitions --- src/tools/__init__.py | 7 ++++++ src/tools/index.py | 58 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 src/tools/index.py diff --git a/src/tools/__init__.py b/src/tools/__init__.py index 8ac42a5..374476a 100644 --- a/src/tools/__init__.py +++ b/src/tools/__init__.py @@ -4,6 +4,11 @@ This module contains all the MCP tools for Couchbase operations. """ +# Index tools +from .index import ( + list_indexes, +) + # Key-Value tools from .kv import ( delete_document_by_id, @@ -40,6 +45,7 @@ delete_document_by_id, get_schema_for_collection, run_sql_plus_plus_query, + list_indexes, ] __all__ = [ @@ -55,6 +61,7 @@ "delete_document_by_id", "get_schema_for_collection", "run_sql_plus_plus_query", + "list_indexes", # Convenience "ALL_TOOLS", ] diff --git a/src/tools/index.py b/src/tools/index.py new file mode 100644 index 0000000..aa8dca4 --- /dev/null +++ b/src/tools/index.py @@ -0,0 +1,58 @@ +""" +Tools for index operations. + +This module contains tools for listing and managing indexes in the Couchbase cluster. +""" + +import logging +from typing import Any + +from mcp.server.fastmcp import Context + +from utils.constants import MCP_SERVER_NAME +from utils.context import get_cluster_connection + +logger = logging.getLogger(f"{MCP_SERVER_NAME}.tools.index") + + +def list_indexes(ctx: Context, bucket_name: str | None = None) -> list[dict[str, Any]]: + """List all indexes in the cluster or filter by bucket name. + Returns a list of indexes with their names, definitions, and metadata. + Each index entry includes: name, bucket, scope, collection, state, index type, and index key definitions. + If bucket_name is provided, only indexes for that bucket are returned. + """ + cluster = get_cluster_connection(ctx) + + try: + # Query system catalog for index information + if bucket_name: + # Filter indexes by bucket + query = "SELECT idx.* FROM system:indexes AS idx WHERE bucket_id = $bucket_name ORDER BY bucket_id, scope_id, keyspace_id, name" + result = cluster.query(query, bucket_name=bucket_name) + else: + # Get all accessible indexes + query = "SELECT idx.* FROM system:indexes AS idx ORDER BY bucket_id, scope_id, keyspace_id, name" + result = cluster.query(query) + + indexes = [] + for row in result: + # Extract relevant index information + index_info = { + "name": row.get("name"), + "bucket": row.get("bucket_id"), + "scope": row.get("scope_id"), + "collection": row.get("keyspace_id"), + "state": row.get("state"), + "index_type": row.get("using", "GSI"), + "is_primary": row.get("is_primary", False), + "index_key": row.get("index_key", []), + "condition": row.get("condition"), + "partition": row.get("partition"), + } + indexes.append(index_info) + + logger.info(f"Found {len(indexes)} indexes") + return indexes + except Exception as e: + logger.error(f"Error listing indexes: {e}") + raise From 34ed77b6cb0ace757efb1597b2b3cc32792ba40b Mon Sep 17 00:00:00 2001 From: AayushTyagi1 Date: Mon, 20 Oct 2025 02:45:04 +0530 Subject: [PATCH 3/6] DA-1171: Modified: Get all Indexes Based on Bucket Scope Collection --- README.md | 2 +- src/tools/index.py | 57 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 89babac..605dbca 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ An [MCP](https://modelcontextprotocol.io/) server implementation of Couchbase th - Get a list of all the scopes in the specified bucket - Get a list of all the collections in a specified scope and bucket. Note that this tool requires the cluster to have Query service. - Get the structure for a collection -- List all indexes in the cluster with their definitions, optionally filtered by bucket name +- List all indexes in the cluster with their definitions, with optional filtering by bucket, scope, and collection - Get a document by ID from a specified scope and collection - Upsert a document by ID to a specified scope and collection - Delete a document by ID from a specified scope and collection diff --git a/src/tools/index.py b/src/tools/index.py index aa8dca4..6854df9 100644 --- a/src/tools/index.py +++ b/src/tools/index.py @@ -15,24 +15,59 @@ logger = logging.getLogger(f"{MCP_SERVER_NAME}.tools.index") -def list_indexes(ctx: Context, bucket_name: str | None = None) -> list[dict[str, Any]]: - """List all indexes in the cluster or filter by bucket name. +def list_indexes( + ctx: Context, + bucket_name: str | None = None, + scope_name: str | None = None, + collection_name: str | None = None, +) -> list[dict[str, Any]]: + """List all indexes in the cluster with optional filtering by bucket, scope, and collection. Returns a list of indexes with their names, definitions, and metadata. Each index entry includes: name, bucket, scope, collection, state, index type, and index key definitions. - If bucket_name is provided, only indexes for that bucket are returned. + + Args: + ctx: MCP context for cluster connection + bucket_name: Optional bucket name to filter indexes + scope_name: Optional scope name to filter indexes (requires bucket_name) + collection_name: Optional collection name to filter indexes (requires bucket_name and scope_name) + + Returns: + List of dictionaries containing index information """ cluster = get_cluster_connection(ctx) try: - # Query system catalog for index information + # Build query with filters based on provided parameters + query = "SELECT idx.* FROM system:indexes AS idx" + conditions = [] + params = {} + if bucket_name: - # Filter indexes by bucket - query = "SELECT idx.* FROM system:indexes AS idx WHERE bucket_id = $bucket_name ORDER BY bucket_id, scope_id, keyspace_id, name" - result = cluster.query(query, bucket_name=bucket_name) - else: - # Get all accessible indexes - query = "SELECT idx.* FROM system:indexes AS idx ORDER BY bucket_id, scope_id, keyspace_id, name" - result = cluster.query(query) + conditions.append("bucket_id = $bucket_name") + params["bucket_name"] = bucket_name + + if scope_name: + if not bucket_name: + raise ValueError("bucket_name is required when filtering by scope_name") + conditions.append("scope_id = $scope_name") + params["scope_name"] = scope_name + + if collection_name: + if not bucket_name or not scope_name: + raise ValueError( + "bucket_name and scope_name are required when filtering by collection_name" + ) + conditions.append("keyspace_id = $collection_name") + params["collection_name"] = collection_name + + if conditions: + query += " WHERE " + " AND ".join(conditions) + + query += " ORDER BY bucket_id, scope_id, keyspace_id, name" + + # Execute query with parameters + logger.info(f"Executing query: {query} with params: {params}") + result = cluster.query(query, **params) indexes = [] for row in result: From 0bd523122b70e6d16f5ec07099a82a6513150c9e Mon Sep 17 00:00:00 2001 From: AayushTyagi1 Date: Wed, 22 Oct 2025 03:05:23 +0530 Subject: [PATCH 4/6] DA-1171: Modified Filter fields and get mroe indexes than usual --- src/tools/index.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tools/index.py b/src/tools/index.py index 6854df9..30e5665 100644 --- a/src/tools/index.py +++ b/src/tools/index.py @@ -38,7 +38,7 @@ def list_indexes( try: # Build query with filters based on provided parameters - query = "SELECT idx.* FROM system:indexes AS idx" + query = "SELECT * FROM system:all_indexes FROM system:indexes AS idx" conditions = [] params = {} @@ -77,12 +77,10 @@ def list_indexes( "bucket": row.get("bucket_id"), "scope": row.get("scope_id"), "collection": row.get("keyspace_id"), - "state": row.get("state"), "index_type": row.get("using", "GSI"), "is_primary": row.get("is_primary", False), - "index_key": row.get("index_key", []), "condition": row.get("condition"), - "partition": row.get("partition"), + "using": row.get("using"), } indexes.append(index_info) From fc0abd0313997a4db89a4adfb5e88a43a1baf9a1 Mon Sep 17 00:00:00 2001 From: AayushTyagi1 Date: Wed, 22 Oct 2025 03:42:19 +0530 Subject: [PATCH 5/6] DA-1171 Added: Index and index definitions --- src/tools/index.py | 54 +++++++++++++++++++++-------- src/utils/__init__.py | 7 ++++ src/utils/index_utils.py | 75 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 src/utils/index_utils.py diff --git a/src/tools/index.py b/src/tools/index.py index 30e5665..ecc1603 100644 --- a/src/tools/index.py +++ b/src/tools/index.py @@ -11,6 +11,7 @@ from utils.constants import MCP_SERVER_NAME from utils.context import get_cluster_connection +from utils.index_utils import generate_index_definition logger = logging.getLogger(f"{MCP_SERVER_NAME}.tools.index") @@ -22,8 +23,8 @@ def list_indexes( collection_name: str | None = None, ) -> list[dict[str, Any]]: """List all indexes in the cluster with optional filtering by bucket, scope, and collection. - Returns a list of indexes with their names, definitions, and metadata. - Each index entry includes: name, bucket, scope, collection, state, index type, and index key definitions. + Returns a simplified list of indexes with their names, primary flag, and CREATE INDEX definitions. + Excludes sequential scan indexes. For GSI indexes, includes the CREATE INDEX definition. Args: ctx: MCP context for cluster connection @@ -32,13 +33,13 @@ def list_indexes( collection_name: Optional collection name to filter indexes (requires bucket_name and scope_name) Returns: - List of dictionaries containing index information + List of dictionaries with keys: name (str), is_primary (bool), definition (str, GSI only) """ cluster = get_cluster_connection(ctx) try: # Build query with filters based on provided parameters - query = "SELECT * FROM system:all_indexes FROM system:indexes AS idx" + query = "SELECT * FROM system:all_indexes" conditions = [] params = {} @@ -71,20 +72,45 @@ def list_indexes( indexes = [] for row in result: - # Extract relevant index information + # Extract the actual index data from the nested structure + # When querying system:all_indexes, data is wrapped in 'all_indexes' key + index_data = row.get("all_indexes", row) + + # Skip sequential scan indexes + using = index_data.get("using", "").lower() + if using == "sequentialscan": + continue + + # Prepare data for index definition generation + temp_data = { + "name": index_data.get("name"), + "bucket": index_data.get("bucket_id"), + "scope": index_data.get("scope_id"), + "collection": index_data.get("keyspace_id"), + "index_type": index_data.get("using", "gsi"), + "is_primary": index_data.get("is_primary", False), + "index_key": index_data.get("index_key", []), + "condition": index_data.get("condition"), + "partition": index_data.get("partition"), + "using": index_data.get("using", "gsi"), + } + + # Generate index definition for GSI indexes + index_definition = generate_index_definition(temp_data) + + # Only return the essential information index_info = { - "name": row.get("name"), - "bucket": row.get("bucket_id"), - "scope": row.get("scope_id"), - "collection": row.get("keyspace_id"), - "index_type": row.get("using", "GSI"), - "is_primary": row.get("is_primary", False), - "condition": row.get("condition"), - "using": row.get("using"), + "name": index_data.get("name"), + "is_primary": index_data.get("is_primary", False), } + + # Add definition only if it was generated (GSI indexes only) + if index_definition: + index_info["definition"] = index_definition + indexes.append(index_info) - logger.info(f"Found {len(indexes)} indexes") + logger.info(f"Found {len(indexes)} indexes (excluding sequential scans)") return indexes except Exception as e: logger.error(f"Error listing indexes: {e}") diff --git a/src/utils/__init__.py b/src/utils/__init__.py index 1db2e64..0a4daa5 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -34,6 +34,11 @@ get_cluster_connection, ) +# Index utilities +from .index_utils import ( + generate_index_definition, +) + # Note: Individual modules create their own hierarchical loggers using: # logger = logging.getLogger(f"{MCP_SERVER_NAME}.module.name") @@ -46,6 +51,8 @@ # Context "AppContext", "get_cluster_connection", + # Index utilities + "generate_index_definition", # Constants "MCP_SERVER_NAME", "DEFAULT_READ_ONLY_MODE", diff --git a/src/utils/index_utils.py b/src/utils/index_utils.py new file mode 100644 index 0000000..eb18c9a --- /dev/null +++ b/src/utils/index_utils.py @@ -0,0 +1,75 @@ +""" +Utility functions for index operations. + +This module contains helper functions for working with Couchbase indexes. +""" + +import logging +from typing import Any + +from .constants import MCP_SERVER_NAME + +logger = logging.getLogger(f"{MCP_SERVER_NAME}.utils.index_utils") + + +def generate_index_definition(index_data: dict[str, Any]) -> str | None: + """Generate CREATE INDEX statement for GSI indexes. + + Args: + index_data: Dictionary containing index information with keys: + - name: Index name + - bucket: Bucket name + - scope: Scope name (optional) + - collection: Collection name (optional) + - is_primary: Boolean indicating if it's a primary index + - using: Index type (must be "gsi" for definition generation) + - index_key: List of index keys + - condition: WHERE condition (optional) + - partition: PARTITION BY clause (optional) + + Returns: + CREATE INDEX statement string for GSI indexes, None for other types + """ + # Only generate definition for GSI indexes + if index_data.get("using") != "gsi": + return None + + try: + # Start building the definition + if index_data.get("is_primary"): + query_definition = "CREATE PRIMARY INDEX" + else: + query_definition = "CREATE INDEX" + + # Add index name + query_definition += f" `{index_data['name']}`" + + # Add bucket name + query_definition += f" ON `{index_data['bucket']}`" + + # Add scope and collection if they exist + scope = index_data.get("scope") + collection = index_data.get("collection") + if scope and collection: + query_definition += f".`{scope}`.`{collection}`" + + # Add index keys for non-primary indexes + index_keys = index_data.get("index_key", []) + if index_keys and len(index_keys) > 0: + keys_str = ", ".join(str(key) for key in index_keys) + query_definition += f"({keys_str})" + + # Add WHERE condition if exists + condition = index_data.get("condition") + if condition: + query_definition += f" WHERE {condition}" + + # Add PARTITION BY if exists + partition = index_data.get("partition") + if partition: + query_definition += f" PARTITION BY {partition}" + + return query_definition + except Exception as e: + logger.warning(f"Error generating index definition: {e}") + return None From eb3f49a7b10c90022adef543cfe6b2c4063e6240 Mon Sep 17 00:00:00 2001 From: AayushTyagi1 Date: Wed, 22 Oct 2025 03:45:59 +0530 Subject: [PATCH 6/6] DA-1171 Added: Index and index definitions --- src/tools/index.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tools/index.py b/src/tools/index.py index ecc1603..7b5c1e8 100644 --- a/src/tools/index.py +++ b/src/tools/index.py @@ -9,8 +9,8 @@ from mcp.server.fastmcp import Context +from tools.query import run_cluster_query from utils.constants import MCP_SERVER_NAME -from utils.context import get_cluster_connection from utils.index_utils import generate_index_definition logger = logging.getLogger(f"{MCP_SERVER_NAME}.tools.index") @@ -35,8 +35,6 @@ def list_indexes( Returns: List of dictionaries with keys: name (str), is_primary (bool), definition (str, GSI only) """ - cluster = get_cluster_connection(ctx) - try: # Build query with filters based on provided parameters query = "SELECT * FROM system:all_indexes" @@ -68,7 +66,7 @@ def list_indexes( # Execute query with parameters logger.info(f"Executing query: {query} with params: {params}") - result = cluster.query(query, **params) + result = run_cluster_query(ctx, query, **params) indexes = [] for row in result: