couchbase-mcp-server 0.5.0__py3-none-any.whl → 0.5.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- certs/__init__.py +0 -0
- certs/capella_root_ca.pem +19 -0
- {couchbase_mcp_server-0.5.0.dist-info → couchbase_mcp_server-0.5.2.dist-info}/METADATA +19 -13
- couchbase_mcp_server-0.5.2.dist-info/RECORD +19 -0
- {couchbase_mcp_server-0.5.0.dist-info → couchbase_mcp_server-0.5.2.dist-info}/WHEEL +1 -2
- mcp_server.py +0 -1
- tools/__init__.py +10 -0
- tools/index.py +172 -0
- tools/query.py +9 -1
- tools/server.py +41 -0
- utils/__init__.py +7 -0
- utils/index_utils.py +308 -0
- couchbase_mcp_server-0.5.0.dist-info/RECORD +0 -16
- couchbase_mcp_server-0.5.0.dist-info/top_level.txt +0 -3
- {couchbase_mcp_server-0.5.0.dist-info → couchbase_mcp_server-0.5.2.dist-info}/entry_points.txt +0 -0
- {couchbase_mcp_server-0.5.0.dist-info → couchbase_mcp_server-0.5.2.dist-info}/licenses/LICENSE +0 -0
certs/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
|
2
|
+
MIIDFTCCAf2gAwIBAgIRANLVkgOvtaXiQJi0V6qeNtswDQYJKoZIhvcNAQELBQAw
|
|
3
|
+
JDESMBAGA1UECgwJQ291Y2hiYXNlMQ4wDAYDVQQLDAVDbG91ZDAeFw0xOTEyMDYy
|
|
4
|
+
MjEyNTlaFw0yOTEyMDYyMzEyNTlaMCQxEjAQBgNVBAoMCUNvdWNoYmFzZTEOMAwG
|
|
5
|
+
A1UECwwFQ2xvdWQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfvOIi
|
|
6
|
+
enG4Dp+hJu9asdxEMRmH70hDyMXv5ZjBhbo39a42QwR59y/rC/sahLLQuNwqif85
|
|
7
|
+
Fod1DkqgO6Ng3vecSAwyYVkj5NKdycQu5tzsZkghlpSDAyI0xlIPSQjoORA/pCOU
|
|
8
|
+
WOpymA9dOjC1bo6rDyw0yWP2nFAI/KA4Z806XeqLREuB7292UnSsgFs4/5lqeil6
|
|
9
|
+
rL3ooAw/i0uxr/TQSaxi1l8t4iMt4/gU+W52+8Yol0JbXBTFX6itg62ppb/Eugmn
|
|
10
|
+
mQRMgL67ccZs7cJ9/A0wlXencX2ohZQOR3mtknfol3FH4+glQFn27Q4xBCzVkY9j
|
|
11
|
+
KQ20T1LgmGSngBInAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
|
|
12
|
+
FJQOBPvrkU2In1Sjoxt97Xy8+cKNMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B
|
|
13
|
+
AQsFAAOCAQEARgM6XwcXPLSpFdSf0w8PtpNGehmdWijPM3wHb7WZiS47iNen3oq8
|
|
14
|
+
m2mm6V3Z57wbboPpfI+VEzbhiDcFfVnK1CXMC0tkF3fnOG1BDDvwt4jU95vBiNjY
|
|
15
|
+
xdzlTP/Z+qr0cnVbGBSZ+fbXstSiRaaAVcqQyv3BRvBadKBkCyPwo+7svQnScQ5P
|
|
16
|
+
Js7HEHKVms5tZTgKIw1fbmgR2XHleah1AcANB+MAPBCcTgqurqr5G7W2aPSBLLGA
|
|
17
|
+
fRIiVzm7VFLc7kWbp7ENH39HVG6TZzKnfl9zJYeiklo5vQQhGSMhzBsO70z4RRzi
|
|
18
|
+
DPFAN/4qZAgD5q3AFNIq2WWADFQGSwVJhg==
|
|
19
|
+
-----END CERTIFICATE-----
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: couchbase-mcp-server
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Couchbase MCP Server - The Developer Data Platform for Critical Applications in Our AI World
|
|
5
|
-
Author-email: Nithish Raghunandanan <devadvocates@couchbase.com>
|
|
6
|
-
License-Expression: Apache-2.0
|
|
7
5
|
Project-URL: Homepage, https://github.com/Couchbase-Ecosystem/mcp-server-couchbase
|
|
8
6
|
Project-URL: Documentation, https://github.com/Couchbase-Ecosystem/mcp-server-couchbase#readme
|
|
9
7
|
Project-URL: Issues, https://github.com/Couchbase-Ecosystem/mcp-server-couchbase/issues
|
|
8
|
+
Author-email: Nithish Raghunandanan <devadvocates@couchbase.com>
|
|
9
|
+
License-Expression: Apache-2.0
|
|
10
|
+
License-File: LICENSE
|
|
10
11
|
Classifier: Development Status :: 4 - Beta
|
|
11
12
|
Classifier: Programming Language :: Python :: 3
|
|
12
13
|
Classifier: Topic :: Database
|
|
13
|
-
Requires-Python:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
Requires-Dist:
|
|
17
|
-
Requires-Dist:
|
|
18
|
-
Requires-Dist:
|
|
19
|
-
Requires-Dist: mcp[cli]==1.12.0
|
|
14
|
+
Requires-Python: <3.14,>=3.10
|
|
15
|
+
Requires-Dist: click<9.0.0,>=8.2.1
|
|
16
|
+
Requires-Dist: couchbase<5.0.0,>=4.4.0
|
|
17
|
+
Requires-Dist: lark-sqlpp>=0.0.1
|
|
18
|
+
Requires-Dist: mcp[cli]<2.0.0,>=1.20.0
|
|
19
|
+
Requires-Dist: urllib3>=2.0.0
|
|
20
20
|
Provides-Extra: dev
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
|
|
21
|
+
Requires-Dist: pre-commit==4.2.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff==0.12.5; extra == 'dev'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# Couchbase MCP Server
|
|
26
26
|
|
|
@@ -32,6 +32,8 @@ An [MCP](https://modelcontextprotocol.io/) server implementation of Couchbase th
|
|
|
32
32
|
<img width="380" height="200" src="https://glama.ai/mcp/servers/@Couchbase-Ecosystem/mcp-server-couchbase/badge" alt="Couchbase Server MCP server" />
|
|
33
33
|
</a>
|
|
34
34
|
|
|
35
|
+
<!-- mcp-name: io.github.Couchbase-Ecosystem/mcp-server-couchbase -->
|
|
36
|
+
|
|
35
37
|
## Features
|
|
36
38
|
|
|
37
39
|
- Get a list of all the buckets in the cluster
|
|
@@ -43,9 +45,13 @@ An [MCP](https://modelcontextprotocol.io/) server implementation of Couchbase th
|
|
|
43
45
|
- Upsert a document by ID to a specified scope and collection
|
|
44
46
|
- Delete a document by ID from a specified scope and collection
|
|
45
47
|
- Run a [SQL++ query](https://www.couchbase.com/sqlplusplus/) on a specified scope
|
|
48
|
+
- Queries are automatically scoped to the specified bucket and scope, so use collection names directly (e.g., use `SELECT * FROM users` instead of `SELECT * FROM bucket.scope.users`)
|
|
46
49
|
- There is an option in the MCP server, `CB_MCP_READ_ONLY_QUERY_MODE` that is set to true by default to disable running SQL++ queries that change the data or the underlying collection structure. Note that the documents can still be updated by ID.
|
|
47
50
|
- Get the status of the MCP server
|
|
48
51
|
- Check the cluster credentials by connecting to the cluster
|
|
52
|
+
- List all indexes in the cluster with their definitions, with optional filtering by bucket, scope, collection and index name.
|
|
53
|
+
- Get index recommendations from Couchbase Index Advisor for a given SQL++ query to optimize query performance
|
|
54
|
+
- Get cluster health status and list of all running services
|
|
49
55
|
|
|
50
56
|
## Prerequisites
|
|
51
57
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
certs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
certs/capella_root_ca.pem,sha256=SuSjgKclcQQg0kheTRd3dg6B0FUsUy717T5n3xcAU_E,1131
|
|
3
|
+
mcp_server.py,sha256=dvHQR-55JhuXX_bj2hLU0ek411qPp0qHEKqAmPdwjKU,4991
|
|
4
|
+
tools/__init__.py,sha256=otQd8IiEILtl1jgorEO1E9KqL_2xBBlqwnVHejoCPE0,1668
|
|
5
|
+
tools/index.py,sha256=cCBr0ptFBVc-HN5SoCauQAh2DsAP_Is8NPUSa6QcLM0,6682
|
|
6
|
+
tools/kv.py,sha256=NGUs43iuXElj9rYe4RCyCStqoh5y1fUgbg1oWuU4WeQ,2493
|
|
7
|
+
tools/query.py,sha256=tg_MBrk8Pe8G3SCWV7m3RpA562pCuowUzCS7gGO95TI,3941
|
|
8
|
+
tools/server.py,sha256=4krPjBoQmUHUmOE03T0CcCGFJtoaeHVS91oOrYj8JsA,6670
|
|
9
|
+
utils/__init__.py,sha256=Fcbp-VIK0Gwwy7hl1AjV9NBKZsscmOV2vYdTBam5M3A,1361
|
|
10
|
+
utils/config.py,sha256=B6H_JYDn6uxtu9juM924zdvNQgSaHh_u6rYME3_0_xQ,268
|
|
11
|
+
utils/connection.py,sha256=NtAU4pmHMZubSJcs_X_lai9o8dih5mW0RyrRdmyp1Po,2892
|
|
12
|
+
utils/constants.py,sha256=w0zvQ1zMzJBg44Yl3aQW8KfaaRPn0BgPOLEe8xLeLSE,487
|
|
13
|
+
utils/context.py,sha256=XZL4M70BMdFBptJ9sT0zxhEey-EvvoSKZJrP_sb7q-A,2286
|
|
14
|
+
utils/index_utils.py,sha256=W0rvoBXU_2aB9m-HDlLChZoMzvlIX6FUWF6RTsYGfYM,10910
|
|
15
|
+
couchbase_mcp_server-0.5.2.dist-info/METADATA,sha256=N07YqDMLBXra3Qh4gu5hlxCvG_nnLbm0E3sMN2KAQqM,21220
|
|
16
|
+
couchbase_mcp_server-0.5.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
17
|
+
couchbase_mcp_server-0.5.2.dist-info/entry_points.txt,sha256=iU5pF4kIMTnNhoMPHhdH-k8o1Fmxb_iM9qJHCZcL6Ak,57
|
|
18
|
+
couchbase_mcp_server-0.5.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
19
|
+
couchbase_mcp_server-0.5.2.dist-info/RECORD,,
|
mcp_server.py
CHANGED
tools/__init__.py
CHANGED
|
@@ -4,6 +4,9 @@ Couchbase MCP Tools
|
|
|
4
4
|
This module contains all the MCP tools for Couchbase operations.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
# Index tools
|
|
8
|
+
from .index import get_index_advisor_recommendations, list_indexes
|
|
9
|
+
|
|
7
10
|
# Key-Value tools
|
|
8
11
|
from .kv import (
|
|
9
12
|
delete_document_by_id,
|
|
@@ -20,6 +23,7 @@ from .query import (
|
|
|
20
23
|
# Server tools
|
|
21
24
|
from .server import (
|
|
22
25
|
get_buckets_in_cluster,
|
|
26
|
+
get_cluster_health_and_services,
|
|
23
27
|
get_collections_in_scope,
|
|
24
28
|
get_scopes_and_collections_in_bucket,
|
|
25
29
|
get_scopes_in_bucket,
|
|
@@ -40,6 +44,9 @@ ALL_TOOLS = [
|
|
|
40
44
|
delete_document_by_id,
|
|
41
45
|
get_schema_for_collection,
|
|
42
46
|
run_sql_plus_plus_query,
|
|
47
|
+
get_index_advisor_recommendations,
|
|
48
|
+
list_indexes,
|
|
49
|
+
get_cluster_health_and_services,
|
|
43
50
|
]
|
|
44
51
|
|
|
45
52
|
__all__ = [
|
|
@@ -55,6 +62,9 @@ __all__ = [
|
|
|
55
62
|
"delete_document_by_id",
|
|
56
63
|
"get_schema_for_collection",
|
|
57
64
|
"run_sql_plus_plus_query",
|
|
65
|
+
"get_index_advisor_recommendations",
|
|
66
|
+
"list_indexes",
|
|
67
|
+
"get_cluster_health_and_services",
|
|
58
68
|
# Convenience
|
|
59
69
|
"ALL_TOOLS",
|
|
60
70
|
]
|
tools/index.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tools for index operations.
|
|
3
|
+
|
|
4
|
+
This module contains tools for listing and managing indexes in the Couchbase cluster and getting index recommendations using the Couchbase Index Advisor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from mcp.server.fastmcp import Context
|
|
11
|
+
|
|
12
|
+
from tools.query import run_sql_plus_plus_query
|
|
13
|
+
from utils.config import get_settings
|
|
14
|
+
from utils.constants import MCP_SERVER_NAME
|
|
15
|
+
from utils.index_utils import (
|
|
16
|
+
fetch_indexes_from_rest_api,
|
|
17
|
+
process_index_data,
|
|
18
|
+
validate_connection_settings,
|
|
19
|
+
validate_filter_params,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(f"{MCP_SERVER_NAME}.tools.index")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_index_advisor_recommendations(
|
|
26
|
+
ctx: Context, bucket_name: str, scope_name: str, query: str
|
|
27
|
+
) -> dict[str, Any]:
|
|
28
|
+
"""Get index recommendations from Couchbase Index Advisor for a given SQL++ query.
|
|
29
|
+
|
|
30
|
+
The Index Advisor analyzes the query and provides recommendations for optimal indexes.
|
|
31
|
+
This tool works with SELECT, UPDATE, DELETE, or MERGE queries.
|
|
32
|
+
The queries will be run on the specified scope in the specified bucket.
|
|
33
|
+
|
|
34
|
+
Returns a dictionary with:
|
|
35
|
+
- current_used_indexes: Array of currently used indexes (if any)
|
|
36
|
+
- recommended_indexes: Array of recommended secondary indexes (if any)
|
|
37
|
+
- recommended_covering_indexes: Array of recommended covering indexes (if any)
|
|
38
|
+
|
|
39
|
+
Each index object contains:
|
|
40
|
+
- index: The CREATE INDEX SQL++ command
|
|
41
|
+
- statements: Array of statement objects with the query and run count
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
# Build the ADVISOR query
|
|
45
|
+
advisor_query = f"SELECT ADVISOR('{query}') AS advisor_result"
|
|
46
|
+
|
|
47
|
+
logger.info("Running Index Advisor for the provided query")
|
|
48
|
+
|
|
49
|
+
# Execute the ADVISOR function at cluster level using run_sql_plus_plus_query
|
|
50
|
+
advisor_results = run_sql_plus_plus_query(
|
|
51
|
+
ctx, bucket_name, scope_name, advisor_query
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not advisor_results:
|
|
55
|
+
return {
|
|
56
|
+
"message": "No recommendations available",
|
|
57
|
+
"current_used_indexes": [],
|
|
58
|
+
"recommended_indexes": [],
|
|
59
|
+
"recommended_covering_indexes": [],
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# The result is wrapped in advisor_result key
|
|
63
|
+
advisor_data = advisor_results[0].get("advisor_result", {})
|
|
64
|
+
|
|
65
|
+
# Extract the relevant fields with defaults
|
|
66
|
+
response = {
|
|
67
|
+
"current_used_indexes": advisor_data.get("current_used_indexes", []),
|
|
68
|
+
"recommended_indexes": advisor_data.get("recommended_indexes", []),
|
|
69
|
+
"recommended_covering_indexes": advisor_data.get(
|
|
70
|
+
"recommended_covering_indexes", []
|
|
71
|
+
),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Add summary information for better user experience
|
|
75
|
+
response["summary"] = {
|
|
76
|
+
"current_indexes_count": len(response["current_used_indexes"]),
|
|
77
|
+
"recommended_indexes_count": len(response["recommended_indexes"]),
|
|
78
|
+
"recommended_covering_indexes_count": len(
|
|
79
|
+
response["recommended_covering_indexes"]
|
|
80
|
+
),
|
|
81
|
+
"has_recommendations": bool(
|
|
82
|
+
response["recommended_indexes"]
|
|
83
|
+
or response["recommended_covering_indexes"]
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger.info(
|
|
88
|
+
f"Index Advisor completed. Found {response['summary']['recommended_indexes_count']} recommended indexes"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
return response
|
|
92
|
+
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Error running Index Advisor: {e!s}", exc_info=True)
|
|
95
|
+
raise
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def list_indexes(
|
|
99
|
+
ctx: Context,
|
|
100
|
+
bucket_name: str | None = None,
|
|
101
|
+
scope_name: str | None = None,
|
|
102
|
+
collection_name: str | None = None,
|
|
103
|
+
index_name: str | None = None,
|
|
104
|
+
include_raw_index_stats: bool = False,
|
|
105
|
+
) -> list[dict[str, Any]]:
|
|
106
|
+
"""List all indexes in the cluster with optional filtering by bucket, scope, collection, and index name.
|
|
107
|
+
Returns a list of indexes with their names and CREATE INDEX definitions.
|
|
108
|
+
Uses the Index Service REST API (/getIndexStatus) to retrieve index information directly.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
ctx: MCP context for cluster connection
|
|
112
|
+
bucket_name: Optional bucket name to filter indexes
|
|
113
|
+
scope_name: Optional scope name to filter indexes (requires bucket_name)
|
|
114
|
+
collection_name: Optional collection name to filter indexes (requires bucket_name and scope_name)
|
|
115
|
+
index_name: Optional index name to filter indexes (requires bucket_name, scope_name, and collection_name)
|
|
116
|
+
include_raw_index_stats: If True, include raw index stats (as-is from API) in addition
|
|
117
|
+
to cleaned-up version. Default is False.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
List of dictionaries with keys:
|
|
121
|
+
- name (str): Index name
|
|
122
|
+
- definition (str): Cleaned-up CREATE INDEX statement
|
|
123
|
+
- status (str): Current status of the index (e.g., "Ready", "Building", "Deferred")
|
|
124
|
+
- isPrimary (bool): Whether this is a primary index
|
|
125
|
+
- bucket (str): Bucket name where the index exists
|
|
126
|
+
- scope (str): Scope name where the index exists
|
|
127
|
+
- collection (str): Collection name where the index exists
|
|
128
|
+
- raw_index_stats (dict, optional): Complete raw index status object from API including metadata,
|
|
129
|
+
state, keyspace info, etc. (only if include_raw_index_stats=True)
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
# Validate parameters
|
|
133
|
+
validate_filter_params(bucket_name, scope_name, collection_name, index_name)
|
|
134
|
+
|
|
135
|
+
# Get and validate connection settings
|
|
136
|
+
settings = get_settings()
|
|
137
|
+
validate_connection_settings(settings)
|
|
138
|
+
|
|
139
|
+
# Fetch indexes from REST API
|
|
140
|
+
logger.info(
|
|
141
|
+
f"Fetching indexes from REST API for bucket={bucket_name}, "
|
|
142
|
+
f"scope={scope_name}, collection={collection_name}, index={index_name}"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
raw_indexes = fetch_indexes_from_rest_api(
|
|
146
|
+
settings["connection_string"],
|
|
147
|
+
settings["username"],
|
|
148
|
+
settings["password"],
|
|
149
|
+
bucket_name=bucket_name,
|
|
150
|
+
scope_name=scope_name,
|
|
151
|
+
collection_name=collection_name,
|
|
152
|
+
index_name=index_name,
|
|
153
|
+
ca_cert_path=settings.get("ca_cert_path"),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# Process and format the results
|
|
157
|
+
indexes = [
|
|
158
|
+
processed
|
|
159
|
+
for idx in raw_indexes
|
|
160
|
+
if (processed := process_index_data(idx, include_raw_index_stats))
|
|
161
|
+
is not None
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
logger.info(
|
|
165
|
+
f"Found {len(indexes)} indexes from REST API "
|
|
166
|
+
f"(include_raw_index_stats={include_raw_index_stats})"
|
|
167
|
+
)
|
|
168
|
+
return indexes
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.error(f"Error listing indexes: {e}", exc_info=True)
|
|
172
|
+
raise
|
tools/query.py
CHANGED
|
@@ -39,7 +39,15 @@ def get_schema_for_collection(
|
|
|
39
39
|
def run_sql_plus_plus_query(
|
|
40
40
|
ctx: Context, bucket_name: str, scope_name: str, query: str
|
|
41
41
|
) -> list[dict[str, Any]]:
|
|
42
|
-
"""Run a SQL++ query on a scope and return the results as a list of JSON objects.
|
|
42
|
+
"""Run a SQL++ query on a scope and return the results as a list of JSON objects.
|
|
43
|
+
|
|
44
|
+
The query will be run on the specified scope in the specified bucket.
|
|
45
|
+
The query should use collection names directly without bucket/scope prefixes, as the scope context is automatically set.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
query = "SELECT * FROM users WHERE age > 18"
|
|
49
|
+
# Incorrect: "SELECT * FROM bucket.scope.users WHERE age > 18"
|
|
50
|
+
"""
|
|
43
51
|
cluster = get_cluster_connection(ctx)
|
|
44
52
|
|
|
45
53
|
bucket = connect_to_bucket(cluster, bucket_name)
|
tools/server.py
CHANGED
|
@@ -4,6 +4,7 @@ Tools for server operations.
|
|
|
4
4
|
This module contains tools for getting the server status, testing the connection, and getting the buckets in the cluster, the scopes and collections in the bucket.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import json
|
|
7
8
|
import logging
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
@@ -137,3 +138,43 @@ def get_collections_in_scope(
|
|
|
137
138
|
ctx, query, bucket_name=bucket_name, scope_name=scope_name
|
|
138
139
|
)
|
|
139
140
|
return [result["collection_name"] for result in results]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_cluster_health_and_services(
|
|
144
|
+
ctx: Context, bucket_name: str | None = None
|
|
145
|
+
) -> dict[str, Any]:
|
|
146
|
+
"""Get cluster health status and list of all running services.
|
|
147
|
+
|
|
148
|
+
This tool provides health monitoring by:
|
|
149
|
+
- Getting health status of all running services with latency information (via ping)
|
|
150
|
+
- Listing all services running on the cluster with their endpoints
|
|
151
|
+
- Showing connection status and node information for each service
|
|
152
|
+
|
|
153
|
+
If bucket_name is provided, it actively pings services from the perspective of the bucket.
|
|
154
|
+
Otherwise, it uses cluster-level ping to get the health status of the cluster.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
- Cluster health status with service-level connection details and latency measurements
|
|
158
|
+
"""
|
|
159
|
+
try:
|
|
160
|
+
cluster = get_cluster_connection(ctx)
|
|
161
|
+
|
|
162
|
+
if bucket_name:
|
|
163
|
+
# Ping services from the perspective of the bucket
|
|
164
|
+
bucket = connect_to_bucket(cluster, bucket_name)
|
|
165
|
+
result = bucket.ping().as_json()
|
|
166
|
+
else:
|
|
167
|
+
# Ping services from the perspective of the cluster
|
|
168
|
+
result = cluster.ping().as_json()
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
"status": "success",
|
|
172
|
+
"data": json.loads(result),
|
|
173
|
+
}
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.error(f"Error getting cluster health: {e}")
|
|
176
|
+
return {
|
|
177
|
+
"status": "error",
|
|
178
|
+
"error": str(e),
|
|
179
|
+
"message": "Failed to get cluster health and services information",
|
|
180
|
+
}
|
utils/__init__.py
CHANGED
|
@@ -34,6 +34,11 @@ from .context import (
|
|
|
34
34
|
get_cluster_connection,
|
|
35
35
|
)
|
|
36
36
|
|
|
37
|
+
# Index utilities
|
|
38
|
+
from .index_utils import (
|
|
39
|
+
fetch_indexes_from_rest_api,
|
|
40
|
+
)
|
|
41
|
+
|
|
37
42
|
# Note: Individual modules create their own hierarchical loggers using:
|
|
38
43
|
# logger = logging.getLogger(f"{MCP_SERVER_NAME}.module.name")
|
|
39
44
|
|
|
@@ -46,6 +51,8 @@ __all__ = [
|
|
|
46
51
|
# Context
|
|
47
52
|
"AppContext",
|
|
48
53
|
"get_cluster_connection",
|
|
54
|
+
# Index utilities
|
|
55
|
+
"fetch_indexes_from_rest_api",
|
|
49
56
|
# Constants
|
|
50
57
|
"MCP_SERVER_NAME",
|
|
51
58
|
"DEFAULT_READ_ONLY_MODE",
|
utils/index_utils.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility functions for index operations.
|
|
3
|
+
|
|
4
|
+
This module contains helper functions for working with Couchbase indexes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
from importlib.resources import files
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import urlparse
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from .constants import MCP_SERVER_NAME
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(f"{MCP_SERVER_NAME}.utils.index_utils")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def validate_filter_params(
|
|
21
|
+
bucket_name: str | None,
|
|
22
|
+
scope_name: str | None,
|
|
23
|
+
collection_name: str | None,
|
|
24
|
+
index_name: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Validate that filter parameters are provided in the correct hierarchy."""
|
|
27
|
+
if scope_name and not bucket_name:
|
|
28
|
+
raise ValueError("bucket_name is required when filtering by scope_name")
|
|
29
|
+
if collection_name and (not bucket_name or not scope_name):
|
|
30
|
+
raise ValueError(
|
|
31
|
+
"bucket_name and scope_name are required when filtering by collection_name"
|
|
32
|
+
)
|
|
33
|
+
if index_name and (not bucket_name or not scope_name or not collection_name):
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"bucket_name, scope_name, and collection_name are required when filtering by index_name"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def validate_connection_settings(settings: dict[str, Any]) -> None:
|
|
40
|
+
"""Validate that required connection settings are present."""
|
|
41
|
+
required = ["connection_string", "username", "password"]
|
|
42
|
+
missing = [key for key in required if not settings.get(key)]
|
|
43
|
+
if missing:
|
|
44
|
+
raise ValueError(f"Missing required connection settings: {', '.join(missing)}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def clean_index_definition(definition: Any) -> str:
|
|
48
|
+
"""Clean up index definition string by removing quotes and escape characters."""
|
|
49
|
+
if isinstance(definition, str) and definition:
|
|
50
|
+
return definition.strip('"').replace('\\"', '"')
|
|
51
|
+
return ""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def process_index_data(
|
|
55
|
+
idx: dict[str, Any], include_raw_index_stats: bool
|
|
56
|
+
) -> dict[str, Any] | None:
|
|
57
|
+
"""Process raw index data into formatted index info.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
idx: Raw index data from the API
|
|
61
|
+
include_raw_index_stats: Whether to include complete raw stats in the output
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Formatted index info dictionary, or None if the index should be skipped (e.g., no name).
|
|
65
|
+
"""
|
|
66
|
+
name = idx.get("name", "")
|
|
67
|
+
if not name:
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
# Start with name and optional definition
|
|
71
|
+
index_info: dict[str, Any] = {"name": name}
|
|
72
|
+
|
|
73
|
+
clean_def = clean_index_definition(idx.get("definition", ""))
|
|
74
|
+
if clean_def:
|
|
75
|
+
index_info["definition"] = clean_def
|
|
76
|
+
|
|
77
|
+
# Copy standard fields from raw index data
|
|
78
|
+
standard_fields = ["status", "bucket", "scope", "collection"]
|
|
79
|
+
for field in standard_fields:
|
|
80
|
+
if field in idx:
|
|
81
|
+
index_info[field] = idx[field]
|
|
82
|
+
|
|
83
|
+
# Always include isPrimary as a boolean
|
|
84
|
+
index_info["isPrimary"] = idx.get("isPrimary", False)
|
|
85
|
+
|
|
86
|
+
# Optionally include complete raw stats
|
|
87
|
+
if include_raw_index_stats:
|
|
88
|
+
index_info["raw_index_stats"] = idx
|
|
89
|
+
|
|
90
|
+
return index_info
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _get_capella_root_ca_path() -> str:
|
|
94
|
+
"""Get the path to the Capella root CA certificate.
|
|
95
|
+
|
|
96
|
+
Uses importlib.resources to locate the certificate file, which works when the package is installed with fallback for development.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Path to the Capella root CA certificate file.
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
# Use importlib.resources to get the certificate path (works for installed packages)
|
|
103
|
+
cert_file = files("certs").joinpath("capella_root_ca.pem")
|
|
104
|
+
# Convert to string path - this works for both installed packages and dev mode
|
|
105
|
+
return str(cert_file)
|
|
106
|
+
except (ImportError, FileNotFoundError, TypeError):
|
|
107
|
+
# Fallback for development: use src/certs/ directory
|
|
108
|
+
utils_dir = os.path.dirname(os.path.abspath(__file__))
|
|
109
|
+
src_dir = os.path.dirname(utils_dir)
|
|
110
|
+
fallback_path = os.path.join(src_dir, "certs", "capella_root_ca.pem")
|
|
111
|
+
|
|
112
|
+
if os.path.exists(fallback_path):
|
|
113
|
+
logger.info(f"Using fallback certificate path: {fallback_path}")
|
|
114
|
+
return fallback_path
|
|
115
|
+
|
|
116
|
+
# If we still can't find it, log a warning and return the fallback path anyway
|
|
117
|
+
logger.warning(
|
|
118
|
+
f"Could not locate Capella root CA certificate at {fallback_path}. "
|
|
119
|
+
"SSL verification may fail for Capella connections."
|
|
120
|
+
)
|
|
121
|
+
return fallback_path
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _extract_hosts_from_connection_string(connection_string: str) -> list[str]:
|
|
125
|
+
"""Extract all hosts from a Couchbase connection string.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
connection_string: Connection string like 'couchbase://host' or 'couchbases://host1,host2,host3'
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
List of hosts extracted from the connection string
|
|
132
|
+
"""
|
|
133
|
+
# Parse the connection string
|
|
134
|
+
parsed = urlparse(connection_string)
|
|
135
|
+
|
|
136
|
+
# If there's a netloc (host), extract all hosts
|
|
137
|
+
if parsed.netloc:
|
|
138
|
+
# Split by comma to handle multiple hosts
|
|
139
|
+
# Remove port if present from each host
|
|
140
|
+
hosts = [host.split(":")[0].strip() for host in parsed.netloc.split(",")]
|
|
141
|
+
return hosts
|
|
142
|
+
|
|
143
|
+
# Fallback: try to extract manually
|
|
144
|
+
# Handle cases like 'couchbase://host:8091' or just 'host'
|
|
145
|
+
host_part = connection_string.replace("couchbase://", "").replace(
|
|
146
|
+
"couchbases://", ""
|
|
147
|
+
)
|
|
148
|
+
host_part = host_part.split("/")[0]
|
|
149
|
+
hosts = [host.split(":")[0].strip() for host in host_part.split(",")]
|
|
150
|
+
return hosts
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _determine_ssl_verification(
|
|
154
|
+
connection_string: str, ca_cert_path: str | None
|
|
155
|
+
) -> bool | str:
|
|
156
|
+
"""Determine SSL verification setting based on connection string and cert path.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
connection_string: Couchbase connection string
|
|
160
|
+
ca_cert_path: Optional path to CA certificate
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
SSL verification setting (bool or path to cert file)
|
|
164
|
+
"""
|
|
165
|
+
is_tls_enabled = connection_string.lower().startswith("couchbases://")
|
|
166
|
+
is_capella_connection = connection_string.lower().endswith(".cloud.couchbase.com")
|
|
167
|
+
|
|
168
|
+
# Priority 1: Capella connections always use Capella root CA
|
|
169
|
+
if is_capella_connection:
|
|
170
|
+
capella_ca = _get_capella_root_ca_path()
|
|
171
|
+
if os.path.exists(capella_ca):
|
|
172
|
+
logger.info(
|
|
173
|
+
f"Capella connection detected, using Capella root CA: {capella_ca}"
|
|
174
|
+
)
|
|
175
|
+
return capella_ca
|
|
176
|
+
logger.warning(
|
|
177
|
+
f"Capella CA certificate not found at {capella_ca}, "
|
|
178
|
+
"falling back to system CA bundle"
|
|
179
|
+
)
|
|
180
|
+
return True
|
|
181
|
+
|
|
182
|
+
# Priority 2: Non-Capella TLS connections use provided cert or system CA bundle
|
|
183
|
+
if is_tls_enabled:
|
|
184
|
+
if ca_cert_path:
|
|
185
|
+
logger.info(f"Using provided CA certificate: {ca_cert_path}")
|
|
186
|
+
return ca_cert_path
|
|
187
|
+
logger.info("Using system CA bundle for SSL verification")
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
# Priority 3: Non-TLS connections (HTTP), disable SSL verification
|
|
191
|
+
logger.info("Non-TLS connection, SSL verification disabled")
|
|
192
|
+
return False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _build_query_params(
|
|
196
|
+
bucket_name: str | None,
|
|
197
|
+
scope_name: str | None,
|
|
198
|
+
collection_name: str | None,
|
|
199
|
+
index_name: str | None = None,
|
|
200
|
+
) -> dict[str, str]:
|
|
201
|
+
"""Build query parameters for the index REST API.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
bucket_name: Optional bucket name
|
|
205
|
+
scope_name: Optional scope name
|
|
206
|
+
collection_name: Optional collection name
|
|
207
|
+
index_name: Optional index name
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Dictionary of query parameters
|
|
211
|
+
"""
|
|
212
|
+
params = {}
|
|
213
|
+
if bucket_name:
|
|
214
|
+
params["bucket"] = bucket_name
|
|
215
|
+
if scope_name:
|
|
216
|
+
params["scope"] = scope_name
|
|
217
|
+
if collection_name:
|
|
218
|
+
params["collection"] = collection_name
|
|
219
|
+
if index_name:
|
|
220
|
+
params["index"] = index_name
|
|
221
|
+
return params
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def fetch_indexes_from_rest_api(
|
|
225
|
+
connection_string: str,
|
|
226
|
+
username: str,
|
|
227
|
+
password: str,
|
|
228
|
+
bucket_name: str | None = None,
|
|
229
|
+
scope_name: str | None = None,
|
|
230
|
+
collection_name: str | None = None,
|
|
231
|
+
index_name: str | None = None,
|
|
232
|
+
ca_cert_path: str | None = None,
|
|
233
|
+
timeout: int = 30,
|
|
234
|
+
) -> list[dict[str, Any]]:
|
|
235
|
+
"""Fetch indexes from Couchbase Index Service REST API.
|
|
236
|
+
|
|
237
|
+
Uses the /getIndexStatus endpoint to retrieve index information.
|
|
238
|
+
This endpoint returns indexes with their definitions directly from the Index Service.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
connection_string: Couchbase connection string (may contain multiple hosts)
|
|
242
|
+
username: Username for authentication
|
|
243
|
+
password: Password for authentication
|
|
244
|
+
bucket_name: Optional bucket name to filter indexes
|
|
245
|
+
scope_name: Optional scope name to filter indexes
|
|
246
|
+
collection_name: Optional collection name to filter indexes
|
|
247
|
+
index_name: Optional index name to filter indexes
|
|
248
|
+
ca_cert_path: Optional path to CA certificate for SSL verification.
|
|
249
|
+
If not provided and using Capella, will use Capella root CA.
|
|
250
|
+
timeout: Request timeout in seconds (default: 30)
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
List of index status dictionaries containing name, definition, and other metadata
|
|
254
|
+
"""
|
|
255
|
+
# Extract all hosts from connection string
|
|
256
|
+
hosts = _extract_hosts_from_connection_string(connection_string)
|
|
257
|
+
|
|
258
|
+
# Determine protocol and port based on whether TLS is enabled
|
|
259
|
+
is_tls_enabled = connection_string.lower().startswith("couchbases://")
|
|
260
|
+
protocol = "https" if is_tls_enabled else "http"
|
|
261
|
+
port = 19102 if is_tls_enabled else 9102
|
|
262
|
+
|
|
263
|
+
logger.info(
|
|
264
|
+
f"TLS {'enabled' if is_tls_enabled else 'disabled'}, "
|
|
265
|
+
f"using {protocol.upper()} with port {port}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Build query parameters and determine SSL verification
|
|
269
|
+
params = _build_query_params(bucket_name, scope_name, collection_name, index_name)
|
|
270
|
+
verify_ssl = _determine_ssl_verification(connection_string, ca_cert_path)
|
|
271
|
+
|
|
272
|
+
# Try each host one by one until we get a successful response
|
|
273
|
+
last_error = None
|
|
274
|
+
for host in hosts:
|
|
275
|
+
try:
|
|
276
|
+
url = f"{protocol}://{host}:{port}/getIndexStatus"
|
|
277
|
+
logger.info(
|
|
278
|
+
f"Attempting to fetch indexes from: {url} with params: {params}"
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
response = httpx.get(
|
|
282
|
+
url,
|
|
283
|
+
params=params,
|
|
284
|
+
auth=(username, password),
|
|
285
|
+
verify=verify_ssl,
|
|
286
|
+
timeout=timeout,
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
response.raise_for_status()
|
|
290
|
+
data = response.json()
|
|
291
|
+
indexes = data.get("status", [])
|
|
292
|
+
|
|
293
|
+
logger.info(f"Successfully fetched {len(indexes)} indexes from {host}")
|
|
294
|
+
return indexes
|
|
295
|
+
|
|
296
|
+
except httpx.HTTPError as e:
|
|
297
|
+
logger.warning(f"Failed to fetch indexes from {host}: {e}")
|
|
298
|
+
last_error = e
|
|
299
|
+
except Exception as e:
|
|
300
|
+
logger.warning(f"Unexpected error when fetching from {host}: {e}")
|
|
301
|
+
last_error = e
|
|
302
|
+
|
|
303
|
+
# If we get here, all hosts failed
|
|
304
|
+
error_msg = f"Failed to fetch indexes from all hosts: {hosts}"
|
|
305
|
+
if last_error:
|
|
306
|
+
error_msg += f". Last error: {last_error}"
|
|
307
|
+
logger.error(error_msg)
|
|
308
|
+
raise RuntimeError(error_msg)
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
mcp_server.py,sha256=QGKbHL5fnQQnFkZZvK89eEWR6HcmF_0JqV_E6lke3M8,5031
|
|
2
|
-
couchbase_mcp_server-0.5.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
3
|
-
tools/__init__.py,sha256=1FRntOrRYPiITHteCiEu0DrIBEesNgpeeLkvzFpRNv4,1355
|
|
4
|
-
tools/kv.py,sha256=NGUs43iuXElj9rYe4RCyCStqoh5y1fUgbg1oWuU4WeQ,2493
|
|
5
|
-
tools/query.py,sha256=fSSG7XuxnND1lHwI-r8y_M3h6f82vCXxCwB_A2wHN-M,3598
|
|
6
|
-
tools/server.py,sha256=iv_UH4OiBnlPYSSEt1rs3LFeYmIZb_c5mEEYBToS8gU,5188
|
|
7
|
-
utils/__init__.py,sha256=eVRW08PAfJXrB3cYbmHIj3v6kfa1-a9dlNXsEOB0igU,1223
|
|
8
|
-
utils/config.py,sha256=B6H_JYDn6uxtu9juM924zdvNQgSaHh_u6rYME3_0_xQ,268
|
|
9
|
-
utils/connection.py,sha256=NtAU4pmHMZubSJcs_X_lai9o8dih5mW0RyrRdmyp1Po,2892
|
|
10
|
-
utils/constants.py,sha256=w0zvQ1zMzJBg44Yl3aQW8KfaaRPn0BgPOLEe8xLeLSE,487
|
|
11
|
-
utils/context.py,sha256=XZL4M70BMdFBptJ9sT0zxhEey-EvvoSKZJrP_sb7q-A,2286
|
|
12
|
-
couchbase_mcp_server-0.5.0.dist-info/METADATA,sha256=G_R6LVJ0LqtBhF4v58z1-qRms3YgHjDpLLDfGwlEI3Q,20630
|
|
13
|
-
couchbase_mcp_server-0.5.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
couchbase_mcp_server-0.5.0.dist-info/entry_points.txt,sha256=iU5pF4kIMTnNhoMPHhdH-k8o1Fmxb_iM9qJHCZcL6Ak,57
|
|
15
|
-
couchbase_mcp_server-0.5.0.dist-info/top_level.txt,sha256=cQeSKgjLtHXStynG6fJkj8o4tBep9OkDai_4h5bf_4I,23
|
|
16
|
-
couchbase_mcp_server-0.5.0.dist-info/RECORD,,
|
{couchbase_mcp_server-0.5.0.dist-info → couchbase_mcp_server-0.5.2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{couchbase_mcp_server-0.5.0.dist-info → couchbase_mcp_server-0.5.2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|