couchbase-mcp-server 0.4.0rc1__py3-none-any.whl → 0.5.1__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.
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=t-TFlzAPH0qUILFjX33n6VnEvTgWAjCRy3HrKkKosWE,4416
2
- couchbase_mcp_server-0.4.0rc1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
3
- tools/__init__.py,sha256=jVvVnpoSJrYFmxAnx2vkyAuKZ1-Ti1fzdIgbO0yqAcM,1097
4
- tools/kv.py,sha256=LDDASE4nD4oiTkDzSSYIrmvleqn7e8W-14UOql_SWK8,2199
5
- tools/query.py,sha256=BYpJkgiCAINqgreW22uk-6JjrKrZkswGYKRhU1l-jLI,2901
6
- tools/server.py,sha256=a4rDDDYO7WNTcKN8KjiQlnsIZ1_ydlAVgIy5nxxic5g,3251
7
- utils/__init__.py,sha256=E7Puxqate6J5xdNWSzLFU33UfsbSGE9B4aiMB696v04,1353
8
- utils/config.py,sha256=vCZfyhQXnbTPwcJi3YfLTXqqirCeHVTSyHui5_aGchs,1136
9
- utils/connection.py,sha256=2a_syAAMFE2zHEFMzsOJGz4Vhz6Tq3lgna3qfhKva0w,1520
10
- utils/constants.py,sha256=w0zvQ1zMzJBg44Yl3aQW8KfaaRPn0BgPOLEe8xLeLSE,487
11
- utils/context.py,sha256=pepc4sCUsmx0itpoJ28_TS03mSXJbQhgTZcN7NAvvTg,3013
12
- couchbase_mcp_server-0.4.0rc1.dist-info/METADATA,sha256=_hBu07b23dHcalDuRUAS_IM-3N_yxggsKbxGGn-WM6E,19329
13
- couchbase_mcp_server-0.4.0rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
- couchbase_mcp_server-0.4.0rc1.dist-info/entry_points.txt,sha256=iU5pF4kIMTnNhoMPHhdH-k8o1Fmxb_iM9qJHCZcL6Ak,57
15
- couchbase_mcp_server-0.4.0rc1.dist-info/top_level.txt,sha256=cQeSKgjLtHXStynG6fJkj8o4tBep9OkDai_4h5bf_4I,23
16
- couchbase_mcp_server-0.4.0rc1.dist-info/RECORD,,
@@ -1,3 +0,0 @@
1
- mcp_server
2
- tools
3
- utils